UNPKG

@sovryn-zero/lib-ethers

Version:
1,113 lines (874 loc) 43.9 kB
import chai, { expect, assert } from "chai"; import chaiAsPromised from "chai-as-promised"; import chaiSpies from "chai-spies"; import { AddressZero } from "@ethersproject/constants"; import { BigNumber } from "@ethersproject/bignumber"; import { Signer } from "@ethersproject/abstract-signer"; import { ethers, network, deployLiquity } from "hardhat"; import { Decimal, Decimalish, Trove, StabilityDeposit, LiquityReceipt, SuccessfulReceipt, SentLiquityTransaction, TroveCreationParams, Fees, ZUSD_LIQUIDATION_RESERVE, MAXIMUM_BORROWING_RATE, MINIMUM_BORROWING_RATE, ZUSD_MINIMUM_DEBT, ZUSD_MINIMUM_NET_DEBT } from "@sovryn-zero/lib-base"; import { HintHelpers } from "../types"; import { PopulatableEthersLiquity, PopulatedEthersLiquityTransaction, _redeemMaxIterations } from "../src/PopulatableEthersLiquity"; import { _LiquityDeploymentJSON } from "../src/contracts"; import { _connectToDeployment } from "../src/EthersLiquityConnection"; import { EthersLiquity } from "../src/EthersLiquity"; import { ReadableEthersLiquity } from "../src/ReadableEthersLiquity"; import mockBalanceRedirectPresaleAbi from "../abi/MockBalanceRedirectPresale.json"; const provider = ethers.provider; chai.use(chaiAsPromised); chai.use(chaiSpies); const connectToDeployment = async ( deployment: _LiquityDeploymentJSON, signer: Signer, frontendTag?: string ) => EthersLiquity._from( _connectToDeployment(deployment, signer, { userAddress: await signer.getAddress(), frontendTag }) ); const increaseTime = async (timeJumpSeconds: number) => { await provider.send("evm_increaseTime", [timeJumpSeconds]); }; function assertStrictEqual<T, U extends T>( actual: T, expected: U, message?: string ): asserts actual is U { assert.strictEqual(actual, expected, message); } function assertDefined<T>(actual: T | undefined): asserts actual is T { assert(actual !== undefined); } const waitForSuccess = async <T extends LiquityReceipt>( tx: Promise<SentLiquityTransaction<unknown, T>> ) => { const receipt = await (await tx).waitForReceipt(); assertStrictEqual(receipt.status, "succeeded" as const); return receipt as Extract<T, SuccessfulReceipt>; }; // TODO make the testcases isolated // describe("EthersLiquity", () => { // let deployer: Signer; // let funder: Signer; // let user: Signer; // let otherUsers: Signer[]; // let deployment: _LiquityDeploymentJSON; // let deployerLiquity: EthersLiquity; // let liquity: EthersLiquity; // let otherLiquities: EthersLiquity[]; // const connectUsers = (users: Signer[]) => // Promise.all(users.map(user => connectToDeployment(deployment, user))); // const openTroves = (users: Signer[], params: TroveCreationParams<Decimalish>[]) => // params // .map((params, i) => () => // Promise.all([ // connectToDeployment(deployment, users[i]), // sendTo(users[i], params.depositCollateral).then(tx => tx.wait()) // ]).then(async ([liquity]) => { // await liquity.openTrove(params, undefined, { gasPrice: 0 }); // }) // ) // .reduce((a, b) => a.then(b), Promise.resolve()); // const sendTo = (user: Signer, value: Decimalish, nonce?: number) => // funder.sendTransaction({ // to: user.getAddress(), // value: Decimal.from(value).hex, // nonce // }); // const sendToEach = async (users: Signer[], value: Decimalish) => { // const txCount = await provider.getTransactionCount(funder.getAddress()); // const txs = await Promise.all(users.map((user, i) => sendTo(user, value, txCount + i))); // // Wait for the last tx to be mined. // await txs[txs.length - 1].wait(); // }; // before(async () => { // [deployer, funder, user, ...otherUsers] = await ethers.getSigners(); // deployment = await deployLiquity(deployer,undefined,undefined,undefined,undefined,undefined,undefined,undefined,undefined,undefined,undefined,true); // liquity = await connectToDeployment(deployment, user); // expect(liquity).to.be.an.instanceOf(EthersLiquity); // }); // // Always setup same initial balance for user // beforeEach(async () => { // const targetBalance = BigNumber.from(Decimal.from(100).hex); // const balance = await user.getBalance(); // const gasPrice = 0; // if (balance.eq(targetBalance)) { // return; // } // if (balance.gt(targetBalance)) { // await user.sendTransaction({ // to: funder.getAddress(), // value: balance.sub(targetBalance), // gasPrice // }); // } else { // await funder.sendTransaction({ // to: user.getAddress(), // value: targetBalance.sub(balance), // gasPrice // }); // } // expect(`${await user.getBalance()}`).to.equal(`${targetBalance}`); // }); // it("should get the price", async () => { // const price = await liquity.getPrice(); // expect(price).to.be.an.instanceOf(Decimal); // }); // describe("findHintForCollateralRatio", () => { // it("should pick the closest approx hint", async () => { // type Resolved<T> = T extends Promise<infer U> ? U : never; // type ApproxHint = Resolved<ReturnType<HintHelpers["getApproxHint"]>>; // const fakeHints: ApproxHint[] = [ // { diff: BigNumber.from(3), hintAddress: "alice", latestRandomSeed: BigNumber.from(1111) }, // { diff: BigNumber.from(4), hintAddress: "bob", latestRandomSeed: BigNumber.from(2222) }, // { diff: BigNumber.from(1), hintAddress: "carol", latestRandomSeed: BigNumber.from(3333) }, // { diff: BigNumber.from(2), hintAddress: "dennis", latestRandomSeed: BigNumber.from(4444) } // ]; // const borrowerOperations = { // estimateAndPopulate: { // openTrove: () => ({}) // } // }; // const hintHelpers = chai.spy.interface({ // getApproxHint: () => Promise.resolve(fakeHints.shift()) // }); // const sortedTroves = chai.spy.interface({ // findInsertPosition: () => Promise.resolve(["fake insert position"]) // }); // const fakeLiquity = new PopulatableEthersLiquity(({ // getNumberOfTroves: () => Promise.resolve(1000000), // getFees: () => Promise.resolve(new Fees(0, 0.99, 1, new Date(), new Date(), false)), // connection: { // signerOrProvider: user, // _contracts: { // borrowerOperations, // hintHelpers, // sortedTroves // } // } // } as unknown) as ReadableEthersLiquity); // const nominalCollateralRatio = Decimal.from(0.5); // const params = Trove.recreate(new Trove(Decimal.from(1), ZUSD_MINIMUM_DEBT)); // const trove = Trove.create(params); // expect(`${trove._nominalCollateralRatio}`).to.equal(`${nominalCollateralRatio}`); // await fakeLiquity.openTrove(params); // expect(hintHelpers.getApproxHint).to.have.been.called.exactly(4); // expect(hintHelpers.getApproxHint).to.have.been.called.with(nominalCollateralRatio.hex); // // returned latestRandomSeed should be passed back on the next call // expect(hintHelpers.getApproxHint).to.have.been.called.with(BigNumber.from(1111)); // expect(hintHelpers.getApproxHint).to.have.been.called.with(BigNumber.from(2222)); // expect(hintHelpers.getApproxHint).to.have.been.called.with(BigNumber.from(3333)); // expect(sortedTroves.findInsertPosition).to.have.been.called.once; // expect(sortedTroves.findInsertPosition).to.have.been.called.with( // nominalCollateralRatio.hex, // "carol" // ); // }); // }); // describe("Trove", () => { // it("should have no Trove initially", async () => { // const trove = await liquity.getTrove(); // expect(trove.isEmpty).to.be.true; // }); // it("should fail to create an undercollateralized Trove", async () => { // const price = await liquity.getPrice(); // const undercollateralized = new Trove(ZUSD_MINIMUM_DEBT.div(price), ZUSD_MINIMUM_DEBT); // await expect(liquity.openTrove(Trove.recreate(undercollateralized))).to.eventually.be.rejected; // }); // it("should fail to create a Trove with too little debt", async () => { // const withTooLittleDebt = new Trove(Decimal.from(50), ZUSD_MINIMUM_DEBT.sub(1)); // await expect(liquity.openTrove(Trove.recreate(withTooLittleDebt))).to.eventually.be.rejected; // }); // const withSomeBorrowing = { depositCollateral: 50, borrowZUSD: ZUSD_MINIMUM_NET_DEBT.add(100) }; // it("should create a Trove with some borrowing", async () => { // const { newTrove, fee } = await liquity.openTrove(withSomeBorrowing); // expect(newTrove).to.deep.equal(Trove.create(withSomeBorrowing)); // expect(`${fee}`).to.equal(`${MINIMUM_BORROWING_RATE.mul(withSomeBorrowing.borrowZUSD)}`); // }); // it("should fail to withdraw all the collateral while the Trove has debt", async () => { // const trove = await liquity.getTrove(); // await expect(liquity.withdrawCollateral(trove.collateral)).to.eventually.be.rejected; // }); // const repaySomeDebt = { repayZUSD: 10 }; // it("should repay some debt", async () => { // const { newTrove, fee } = await liquity.repayZUSD(repaySomeDebt.repayZUSD); // expect(newTrove).to.deep.equal(Trove.create(withSomeBorrowing).adjust(repaySomeDebt)); // expect(`${fee}`).to.equal("0"); // }); // const borrowSomeMore = { borrowZUSD: 20 }; // it("should borrow some more", async () => { // const { newTrove, fee } = await liquity.borrowZUSD(borrowSomeMore.borrowZUSD); // expect(newTrove).to.deep.equal( // Trove.create(withSomeBorrowing).adjust(repaySomeDebt).adjust(borrowSomeMore) // ); // expect(`${fee}`).to.equal(`${MINIMUM_BORROWING_RATE.mul(borrowSomeMore.borrowZUSD)}`); // }); // const depositMoreCollateral = { depositCollateral: 1 }; // it("should deposit more collateral", async () => { // const { newTrove } = await liquity.depositCollateral(depositMoreCollateral.depositCollateral); // expect(newTrove).to.deep.equal( // Trove.create(withSomeBorrowing) // .adjust(repaySomeDebt) // .adjust(borrowSomeMore) // .adjust(depositMoreCollateral) // ); // }); // const repayAndWithdraw = { repayZUSD: 60, withdrawCollateral: 0.5 }; // it("should repay some debt and withdraw some collateral at the same time", async () => { // const { newTrove } = await liquity.adjustTrove(repayAndWithdraw, undefined, { gasPrice: 0 }); // expect(newTrove).to.deep.equal( // Trove.create(withSomeBorrowing) // .adjust(repaySomeDebt) // .adjust(borrowSomeMore) // .adjust(depositMoreCollateral) // .adjust(repayAndWithdraw) // ); // const ethBalance = Decimal.fromBigNumberString(`${await user.getBalance()}`); // expect(`${ethBalance}`).to.equal("100.5"); // }); // const borrowAndDeposit = { borrowZUSD: 60, depositCollateral: 0.5 }; // it("should borrow more and deposit some collateral at the same time", async () => { // const { newTrove, fee } = await liquity.adjustTrove(borrowAndDeposit, undefined, { // gasPrice: 0 // }); // expect(newTrove).to.deep.equal( // Trove.create(withSomeBorrowing) // .adjust(repaySomeDebt) // .adjust(borrowSomeMore) // .adjust(depositMoreCollateral) // .adjust(repayAndWithdraw) // .adjust(borrowAndDeposit) // ); // expect(`${fee}`).to.equal(`${MINIMUM_BORROWING_RATE.mul(borrowAndDeposit.borrowZUSD)}`); // const ethBalance = Decimal.fromBigNumberString(`${await user.getBalance()}`); // expect(`${ethBalance}`).to.equal("99.5"); // }); // it("should close the Trove with some ZUSD from another user", async () => { // const price = await liquity.getPrice(); // const initialTrove = await liquity.getTrove(); // const zusdBalance = await liquity.getZEROBalance(); // const zusdShortage = initialTrove.netDebt.sub(zusdBalance); // let funderTrove = Trove.create({ depositCollateral: 1, borrowZUSD: zusdShortage }); // funderTrove = funderTrove.setDebt(Decimal.max(funderTrove.debt, ZUSD_MINIMUM_DEBT)); // funderTrove = funderTrove.setCollateral(funderTrove.debt.mulDiv(1.51, price)); // const funderLiquity = await connectToDeployment(deployment, funder); // await funderLiquity.openTrove(Trove.recreate(funderTrove)); // await funderLiquity.sendZUSD(await user.getAddress(), zusdShortage); // const { params } = await liquity.closeTrove(); // expect(params).to.deep.equal({ // withdrawCollateral: initialTrove.collateral, // repayZUSD: initialTrove.netDebt // }); // const finalTrove = await liquity.getTrove(); // expect(finalTrove.isEmpty).to.be.true; // }); // }); // describe("SendableEthersLiquity", () => { // it("should parse failed transactions without throwing", async () => { // // By passing a gasLimit, we avoid automatic use of estimateGas which would throw // const tx = await liquity.send.openTrove( // { depositCollateral: 0.01, borrowZUSD: 0.01 }, // undefined, // { gasLimit: 1e6 } // ); // const { status } = await tx.waitForReceipt(); // expect(status).to.equal("failed"); // }); // }); // describe("Frontend", () => { // it("should have no frontend initially", async () => { // const frontend = await liquity.getFrontendStatus(await user.getAddress()); // assertStrictEqual(frontend.status, "unregistered" as const); // }); // it("should register a frontend", async () => { // await liquity.registerFrontend(0.75); // }); // it("should have a frontend now", async () => { // const frontend = await liquity.getFrontendStatus(await user.getAddress()); // assertStrictEqual(frontend.status, "registered" as const); // expect(`${frontend.kickbackRate}`).to.equal("0.75"); // }); // it("other user's deposit should be tagged with the frontend's address", async () => { // const frontendTag = await user.getAddress(); // await funder.sendTransaction({ // to: otherUsers[0].getAddress(), // value: Decimal.from(20.1).hex // }); // const otherLiquity = await connectToDeployment(deployment, otherUsers[0], frontendTag); // await otherLiquity.openTrove({ depositCollateral: 20, borrowZUSD: ZUSD_MINIMUM_DEBT }); // if (deployment.presaleAddress) { // const presale = new ethers.Contract( // deployment.presaleAddress, // mockBalanceRedirectPresaleAbi, // provider // ); // await presale.connect(deployer).closePresale(); // } // await otherLiquity.depositZUSDInStabilityPool(ZUSD_MINIMUM_DEBT); // const deposit = await otherLiquity.getStabilityDeposit(); // expect(deposit.frontendTag).to.equal(frontendTag); // }); // }); // describe("StabilityPool", () => { // before(async () => { // deployment = await deployLiquity(deployer,undefined,undefined,undefined,undefined,undefined,undefined,undefined,undefined,undefined,undefined,true); // [deployerLiquity, liquity, ...otherLiquities] = await connectUsers([ // deployer, // user, // ...otherUsers.slice(0, 1) // ]); // await funder.sendTransaction({ // to: otherUsers[0].getAddress(), // value: ZUSD_MINIMUM_DEBT.div(170).hex // }); // }); // const initialTroveOfDepositor = Trove.create({ // depositCollateral: ZUSD_MINIMUM_DEBT.div(100), // borrowZUSD: ZUSD_MINIMUM_NET_DEBT // }); // const smallStabilityDeposit = Decimal.from(10); // it("should fail if Zero presale is open", async () => { // await expect(liquity.depositZUSDInStabilityPool(smallStabilityDeposit)).to.eventually.be // .rejected; // }); // it("should make a small stability deposit", async () => { // const { newTrove } = await liquity.openTrove(Trove.recreate(initialTroveOfDepositor)); // expect(newTrove).to.deep.equal(initialTroveOfDepositor); // if (deployment.presaleAddress) { // const presale = new ethers.Contract( // deployment.presaleAddress, // mockBalanceRedirectPresaleAbi, // provider // ); // await presale.connect(deployer).closePresale(); // } // const details = await liquity.depositZUSDInStabilityPool(smallStabilityDeposit); // expect(details).to.deep.equal({ // zusdLoss: Decimal.from(0), // newZUSDDeposit: smallStabilityDeposit, // collateralGain: Decimal.from(0), // zeroReward: Decimal.from(0), // change: { // depositZUSD: smallStabilityDeposit // } // }); // }); // const troveWithVeryLowICR = Trove.create({ // depositCollateral: ZUSD_MINIMUM_DEBT.div(180), // borrowZUSD: ZUSD_MINIMUM_NET_DEBT // }); // it("other user should make a Trove with very low ICR", async () => { // const { newTrove } = await otherLiquities[0].openTrove(Trove.recreate(troveWithVeryLowICR)); // const price = await liquity.getPrice(); // expect(Number(`${newTrove.collateralRatio(price)}`)).to.be.below(1.15); // }); // const dippedPrice = Decimal.from(190); // it("the price should take a dip", async () => { // await deployerLiquity.setPrice(dippedPrice); // const price = await liquity.getPrice(); // expect(`${price}`).to.equal(`${dippedPrice}`); // }); // it("should liquidate other user's Trove", async () => { // const details = await liquity.liquidateUpTo(1); // expect(details).to.deep.equal({ // liquidatedAddresses: [await otherUsers[0].getAddress()], // collateralGasCompensation: troveWithVeryLowICR.collateral.mul(0.005), // 0.5% // zusdGasCompensation: ZUSD_LIQUIDATION_RESERVE, // totalLiquidated: new Trove( // troveWithVeryLowICR.collateral // .mul(0.995) // -0.5% gas compensation // .add("0.000000000000000001"), // tiny imprecision // troveWithVeryLowICR.debt // ) // }); // const otherTrove = await otherLiquities[0].getTrove(); // expect(otherTrove.isEmpty).to.be.true; // }); // it("should have a depleted stability deposit and some collateral gain", async () => { // const stabilityDeposit = await liquity.getStabilityDeposit(); // expect(stabilityDeposit).to.deep.equal( // new StabilityDeposit( // smallStabilityDeposit, // Decimal.ZERO, // troveWithVeryLowICR.collateral // .mul(0.995) // -0.5% gas compensation // .mulDiv(smallStabilityDeposit, troveWithVeryLowICR.debt) // .sub("0.000000000000000005"), // tiny imprecision // Decimal.ZERO, // AddressZero // ) // ); // }); // it("the Trove should have received some liquidation shares", async () => { // const trove = await liquity.getTrove(); // expect(trove).to.deep.equal({ // ownerAddress: await user.getAddress(), // status: "open", // ...initialTroveOfDepositor // .addDebt(troveWithVeryLowICR.debt.sub(smallStabilityDeposit)) // .addCollateral( // troveWithVeryLowICR.collateral // .mul(0.995) // -0.5% gas compensation // .mulDiv(troveWithVeryLowICR.debt.sub(smallStabilityDeposit), troveWithVeryLowICR.debt) // .add("0.000000000000000001") // tiny imprecision // ) // }); // }); // it("total should equal the Trove", async () => { // const trove = await liquity.getTrove(); // const numberOfTroves = await liquity.getNumberOfTroves(); // expect(numberOfTroves).to.equal(1); // const total = await liquity.getTotal(); // expect(total).to.deep.equal( // trove.addCollateral("0.000000000000000001") // tiny imprecision // ); // }); // it("should transfer the gains to the Trove", async () => { // const details = await liquity.transferCollateralGainToTrove(); // expect(details).to.deep.equal({ // zusdLoss: smallStabilityDeposit, // newZUSDDeposit: Decimal.ZERO, // zeroReward: Decimal.ZERO, // collateralGain: troveWithVeryLowICR.collateral // .mul(0.995) // -0.5% gas compensation // .mulDiv(smallStabilityDeposit, troveWithVeryLowICR.debt) // .sub("0.000000000000000005"), // tiny imprecision // newTrove: initialTroveOfDepositor // .addDebt(troveWithVeryLowICR.debt.sub(smallStabilityDeposit)) // .addCollateral( // troveWithVeryLowICR.collateral // .mul(0.995) // -0.5% gas compensation // .sub("0.000000000000000005") // tiny imprecision // ) // }); // const stabilityDeposit = await liquity.getStabilityDeposit(); // expect(stabilityDeposit.isEmpty).to.be.true; // }); // describe("when people overstay", () => { // before(async () => { // // Deploy new instances of the contracts, for a clean slate // deployment = await deployLiquity(deployer,undefined,undefined,undefined,undefined,undefined,undefined,undefined,undefined,undefined,undefined,true); // const otherUsersSubset = otherUsers.slice(0, 5); // [deployerLiquity, liquity, ...otherLiquities] = await connectUsers([ // deployer, // user, // ...otherUsersSubset // ]); // if (deployment.presaleAddress) { // const presale = new ethers.Contract( // deployment.presaleAddress, // mockBalanceRedirectPresaleAbi, // provider // ); // await presale.connect(deployer).closePresale(); // } // await sendToEach(otherUsersSubset, 21.1); // let price = Decimal.from(200); // await deployerLiquity.setPrice(price); // // Use this account to print ZUSD // await liquity.openTrove({ depositCollateral: 50, borrowZUSD: 5000 }); // // otherLiquities[0-2] will be independent stability depositors // await liquity.sendZUSD(await otherUsers[0].getAddress(), 3000); // await liquity.sendZUSD(await otherUsers[1].getAddress(), 1000); // await liquity.sendZUSD(await otherUsers[2].getAddress(), 1000); // // otherLiquities[3-4] will be Trove owners whose Troves get liquidated // await otherLiquities[3].openTrove({ depositCollateral: 21, borrowZUSD: 2900 }); // await otherLiquities[4].openTrove({ depositCollateral: 21, borrowZUSD: 2900 }); // await otherLiquities[0].depositZUSDInStabilityPool(3000); // await otherLiquities[1].depositZUSDInStabilityPool(1000); // // otherLiquities[2] doesn't deposit yet // // Tank the price so we can liquidate // price = Decimal.from(150); // await deployerLiquity.setPrice(price); // // Liquidate first victim // await liquity.liquidate(await otherUsers[3].getAddress()); // expect((await otherLiquities[3].getTrove()).isEmpty).to.be.true; // // Now otherLiquities[2] makes their deposit too // await otherLiquities[2].depositZUSDInStabilityPool(1000); // // Liquidate second victim // await liquity.liquidate(await otherUsers[4].getAddress()); // expect((await otherLiquities[4].getTrove()).isEmpty).to.be.true; // // Stability Pool is now empty // expect(`${await liquity.getZUSDInStabilityPool()}`).to.equal("0"); // }); // it("should still be able to withdraw remaining deposit", async () => { // for (const l of [otherLiquities[0], otherLiquities[1], otherLiquities[2]]) { // const stabilityDeposit = await l.getStabilityDeposit(); // await l.withdrawZUSDFromStabilityPool(stabilityDeposit.currentZUSD); // } // }); // }); // }); // describe("Redemption", () => { // const troveCreations = [ // { depositCollateral: 99, borrowZUSD: 4600 }, // { depositCollateral: 20, borrowZUSD: 2000 }, // net debt: 2010 // { depositCollateral: 20, borrowZUSD: 2100 }, // net debt: 2110.5 // { depositCollateral: 20, borrowZUSD: 2200 } // net debt: 2211 // ]; // before(async function () { // if (network.name !== "hardhat") { // // Redemptions are only allowed after a bootstrap phase of 2 weeks. // // Since fast-forwarding only works on Hardhat EVM, skip these tests elsewhere. // this.skip(); // } // // Deploy new instances of the contracts, for a clean slate // deployment = await deployLiquity(deployer,undefined,undefined,undefined,undefined,undefined,undefined,undefined,undefined,undefined,undefined,true); // const otherUsersSubset = otherUsers.slice(0, 3); // [deployerLiquity, liquity, ...otherLiquities] = await connectUsers([ // deployer, // user, // ...otherUsersSubset // ]); // await sendToEach(otherUsersSubset, 20.1); // }); // it("should fail to redeem during the bootstrap phase", async () => { // await liquity.openTrove(troveCreations[0]); // await otherLiquities[0].openTrove(troveCreations[1]); // await otherLiquities[1].openTrove(troveCreations[2]); // await otherLiquities[2].openTrove(troveCreations[3]); // await expect(liquity.redeemZUSD(4326.5, undefined, { gasPrice: 0 })).to.eventually.be.rejected; // }); // const someZUSD = Decimal.from(4326.5); // it("should redeem some ZUSD after the bootstrap phase", async () => { // // Fast-forward 15 days // increaseTime(60 * 60 * 24 * 15); // expect(`${await otherLiquities[0].getCollateralSurplusBalance()}`).to.equal("0"); // expect(`${await otherLiquities[1].getCollateralSurplusBalance()}`).to.equal("0"); // expect(`${await otherLiquities[2].getCollateralSurplusBalance()}`).to.equal("0"); // const expectedTotal = troveCreations // .map(params => Trove.create(params)) // .reduce((a, b) => a.add(b)); // const total = await liquity.getTotal(); // expect(total).to.deep.equal(expectedTotal); // const expectedDetails = { // attemptedZUSDAmount: someZUSD, // actualZUSDAmount: someZUSD, // collateralTaken: someZUSD.div(200), // fee: new Fees(0, 0.99, 2, new Date(), new Date(), false) // .redemptionRate(someZUSD.div(total.debt)) // .mul(someZUSD.div(200)) // }; // const details = await liquity.redeemZUSD(someZUSD, undefined, { gasPrice: 0 }); // expect(details).to.deep.equal(expectedDetails); // const balance = Decimal.fromBigNumberString(`${await provider.getBalance(user.getAddress())}`); // expect(`${balance}`).to.equal( // `${expectedDetails.collateralTaken.sub(expectedDetails.fee).add(100)}` // ); // expect(`${await liquity.getZUSDBalance()}`).to.equal("273.5"); // expect(`${(await otherLiquities[0].getTrove()).debt}`).to.equal( // `${Trove.create(troveCreations[1]).debt.sub( // someZUSD // .sub(Trove.create(troveCreations[2]).netDebt) // .sub(Trove.create(troveCreations[3]).netDebt) // )}` // ); // expect((await otherLiquities[1].getTrove()).isEmpty).to.be.true; // expect((await otherLiquities[2].getTrove()).isEmpty).to.be.true; // }); // it("should claim the collateral surplus after redemption", async () => { // const balanceBefore1 = await provider.getBalance(otherUsers[1].getAddress()); // const balanceBefore2 = await provider.getBalance(otherUsers[2].getAddress()); // expect(`${await otherLiquities[0].getCollateralSurplusBalance()}`).to.equal("0"); // const surplus1 = await otherLiquities[1].getCollateralSurplusBalance(); // const trove1 = Trove.create(troveCreations[2]); // expect(`${surplus1}`).to.equal(`${trove1.collateral.sub(trove1.netDebt.div(200))}`); // const surplus2 = await otherLiquities[2].getCollateralSurplusBalance(); // const trove2 = Trove.create(troveCreations[3]); // expect(`${surplus2}`).to.equal(`${trove2.collateral.sub(trove2.netDebt.div(200))}`); // await otherLiquities[1].claimCollateralSurplus({ gasPrice: 0 }); // await otherLiquities[2].claimCollateralSurplus({ gasPrice: 0 }); // expect(`${await otherLiquities[0].getCollateralSurplusBalance()}`).to.equal("0"); // expect(`${await otherLiquities[1].getCollateralSurplusBalance()}`).to.equal("0"); // expect(`${await otherLiquities[2].getCollateralSurplusBalance()}`).to.equal("0"); // const balanceAfter1 = await provider.getBalance(otherUsers[1].getAddress()); // const balanceAfter2 = await provider.getBalance(otherUsers[2].getAddress()); // expect(`${balanceAfter1}`).to.equal(`${balanceBefore1.add(surplus1.hex)}`); // expect(`${balanceAfter2}`).to.equal(`${balanceBefore2.add(surplus2.hex)}`); // }); // it("borrowing rate should be maxed out now", async () => { // const borrowZUSD = Decimal.from(10); // const { fee, newTrove } = await liquity.borrowZUSD(borrowZUSD); // expect(`${fee}`).to.equal(`${borrowZUSD.mul(MAXIMUM_BORROWING_RATE)}`); // expect(newTrove).to.deep.equal( // Trove.create(troveCreations[0]).adjust({ borrowZUSD }, MAXIMUM_BORROWING_RATE) // ); // }); // }); // describe("Redemption (truncation)", () => { // const troveCreationParams = { depositCollateral: 20, borrowZUSD: 2000 }; // const netDebtPerTrove = Trove.create(troveCreationParams).netDebt; // const amountToAttempt = Decimal.from(3900); // const expectedRedeemable = netDebtPerTrove.mul(2).sub(ZUSD_MINIMUM_NET_DEBT); // before(function () { // if (network.name !== "hardhat") { // // Redemptions are only allowed after a bootstrap phase of 2 weeks. // // Since fast-forwarding only works on Hardhat EVM, skip these tests elsewhere. // this.skip(); // } // }); // beforeEach(async function () { // this.timeout("1m"); // // Deploy new instances of the contracts, for a clean slate // deployment = await deployLiquity(deployer,undefined,undefined,undefined,undefined,undefined,undefined,undefined,undefined,undefined,undefined,true); // const otherUsersSubset = otherUsers.slice(0, 3); // [deployerLiquity, liquity, ...otherLiquities] = await connectUsers([ // deployer, // user, // ...otherUsersSubset // ]); // await sendToEach(otherUsersSubset, 20.1); // await liquity.openTrove({ depositCollateral: 99, borrowZUSD: 5000 }); // await otherLiquities[0].openTrove(troveCreationParams); // await otherLiquities[1].openTrove(troveCreationParams); // await otherLiquities[2].openTrove(troveCreationParams); // increaseTime(60 * 60 * 24 * 15); // }); // it("should truncate the amount if it would put the last Trove below the min debt", async () => { // const redemption = await liquity.populate.redeemZUSD(amountToAttempt); // expect(`${redemption.attemptedZUSDAmount}`).to.equal(`${amountToAttempt}`); // expect(`${redemption.redeemableZUSDAmount}`).to.equal(`${expectedRedeemable}`); // expect(redemption.isTruncated).to.be.true; // const { details } = await waitForSuccess(redemption.send()); // expect(`${details.attemptedZUSDAmount}`).to.equal(`${expectedRedeemable}`); // expect(`${details.actualZUSDAmount}`).to.equal(`${expectedRedeemable}`); // }); // it("should increase the amount to the next lowest redeemable value", async () => { // const increasedRedeemable = expectedRedeemable.add(ZUSD_MINIMUM_NET_DEBT); // const initialRedemption = await liquity.populate.redeemZUSD(amountToAttempt); // const increasedRedemption = await initialRedemption.increaseAmountByMinimumNetDebt(); // expect(`${increasedRedemption.attemptedZUSDAmount}`).to.equal(`${increasedRedeemable}`); // expect(`${increasedRedemption.redeemableZUSDAmount}`).to.equal(`${increasedRedeemable}`); // expect(increasedRedemption.isTruncated).to.be.false; // const { details } = await waitForSuccess(increasedRedemption.send()); // expect(`${details.attemptedZUSDAmount}`).to.equal(`${increasedRedeemable}`); // expect(`${details.actualZUSDAmount}`).to.equal(`${increasedRedeemable}`); // }); // it("should fail to increase the amount if it's not truncated", async () => { // const redemption = await liquity.populate.redeemZUSD(netDebtPerTrove); // expect(redemption.isTruncated).to.be.false; // expect(() => redemption.increaseAmountByMinimumNetDebt()).to.throw( // "can only be called when amount is truncated" // ); // }); // }); // describe("Redemption (gas checks)", function () { // this.timeout("10m"); // const massivePrice = Decimal.from(1000000); // const amountToBorrowPerTrove = Decimal.from(2000); // const netDebtPerTrove = MINIMUM_BORROWING_RATE.add(1).mul(amountToBorrowPerTrove); // const collateralPerTrove = netDebtPerTrove // .add(ZUSD_LIQUIDATION_RESERVE) // .mulDiv(1.5, massivePrice); // const amountToRedeem = netDebtPerTrove.mul(_redeemMaxIterations); // const amountToDeposit = MINIMUM_BORROWING_RATE.add(1) // .mul(amountToRedeem) // .add(ZUSD_LIQUIDATION_RESERVE) // .mulDiv(2, massivePrice); // before(async function () { // if (network.name !== "hardhat") { // // Redemptions are only allowed after a bootstrap phase of 2 weeks. // // Since fast-forwarding only works on Hardhat EVM, skip these tests elsewhere. // this.skip(); // } // // Deploy new instances of the contracts, for a clean slate // deployment = await deployLiquity(deployer,undefined,undefined,undefined,undefined,undefined,undefined,undefined,undefined,undefined,undefined,true); // const otherUsersSubset = otherUsers.slice(0, _redeemMaxIterations); // expect(otherUsersSubset).to.have.length(_redeemMaxIterations); // [deployerLiquity, liquity, ...otherLiquities] = await connectUsers([ // deployer, // user, // ...otherUsersSubset // ]); // await deployerLiquity.setPrice(massivePrice); // await sendToEach(otherUsersSubset, collateralPerTrove); // for (const otherLiquity of otherLiquities) { // await otherLiquity.openTrove( // { // depositCollateral: collateralPerTrove, // borrowZUSD: amountToBorrowPerTrove // }, // undefined, // { gasPrice: 0 } // ); // } // increaseTime(60 * 60 * 24 * 15); // }); // it("should redeem using the maximum iterations and almost all gas", async () => { // await liquity.openTrove({ // depositCollateral: amountToDeposit, // borrowZUSD: amountToRedeem // }); // const { rawReceipt } = await waitForSuccess(liquity.send.redeemZUSD(amountToRedeem)); // const gasUsed = rawReceipt.gasUsed.toNumber(); // // gasUsed is ~half the real used amount because of how refunds work, see: // // https://ethereum.stackexchange.com/a/859/9205 // expect(gasUsed).to.be.at.least(4900000, "should use close to 10M gas"); // }); // }); // describe("Gas estimation", () => { // const troveWithICRBetween = (a: Trove, b: Trove) => a.add(b).multiply(0.5); // let rudeUser: Signer; // let fiveOtherUsers: Signer[]; // let rudeLiquity: EthersLiquity; // before(async function () { // this.timeout("10m"); // if (network.name !== "hardhat") { // this.skip(); // } // this.timeout("1m"); // deployment = await deployLiquity(deployer,undefined,undefined,undefined,undefined,undefined,undefined,undefined,undefined,undefined,undefined,true); // [rudeUser, ...fiveOtherUsers] = otherUsers.slice(0, 6); // [deployerLiquity, liquity, rudeLiquity, ...otherLiquities] = await connectUsers([ // deployer, // user, // rudeUser, // ...fiveOtherUsers // ]); // await openTroves(fiveOtherUsers, [ // { depositCollateral: 20, borrowZUSD: 2040 }, // { depositCollateral: 20, borrowZUSD: 2050 }, // { depositCollateral: 20, borrowZUSD: 2060 }, // { depositCollateral: 20, borrowZUSD: 2070 }, // { depositCollateral: 20, borrowZUSD: 2080 } // ]); // increaseTime(60 * 60 * 24 * 15); // }); // it("should include enough gas for updating lastFeeOperationTime", async () => { // await liquity.openTrove({ depositCollateral: 20, borrowZUSD: 2090 }); // // We just updated lastFeeOperationTime, so this won't anticipate having to update that // // during estimateGas // const tx = await liquity.populate.redeemZUSD(1); // const originalGasEstimate = await provider.estimateGas(tx.rawPopulatedTransaction); // // Fast-forward 2 minutes. // await increaseTime(120); // // Required gas has just went up. // const newGasEstimate = await provider.estimateGas(tx.rawPopulatedTransaction); // const gasIncrease = newGasEstimate.sub(originalGasEstimate).toNumber(); // expect(gasIncrease).to.be.within(4900, 10000); // // This will now have to update lastFeeOperationTime // await waitForSuccess(tx.send()); // // Decay base-rate back to 0 // await increaseTime(100000000); // }); // it("should include enough gas for one extra traversal", async () => { // const troves = await liquity.getTroves({ first: 10, sortedBy: "ascendingCollateralRatio" }); // const trove = await liquity.getTrove(); // const newTrove = troveWithICRBetween(troves[3], troves[4]); // // First, we want to test a non-borrowing case, to make sure we're not passing due to any // // extra gas we add to cover a potential lastFeeOperationTime update // const adjustment = trove.adjustTo(newTrove); // expect(adjustment.borrowZUSD).to.be.undefined; // const tx = await liquity.populate.adjustTrove(adjustment); // const originalGasEstimate = await provider.estimateGas(tx.rawPopulatedTransaction); // // A terribly rude user interferes // const rudeTrove = newTrove.addDebt(1); // const rudeCreation = Trove.recreate(rudeTrove); // await openTroves([rudeUser], [rudeCreation]); // const newGasEstimate = await provider.estimateGas(tx.rawPopulatedTransaction); // const gasIncrease = newGasEstimate.sub(originalGasEstimate).toNumber(); // await waitForSuccess(tx.send()); // expect(gasIncrease).to.be.within(10000, 30000); // assertDefined(rudeCreation.borrowZUSD); // const zusdShortage = rudeTrove.debt.sub(rudeCreation.borrowZUSD); // await liquity.sendZUSD(await rudeUser.getAddress(), zusdShortage); // await rudeLiquity.closeTrove({ gasPrice: 0 }); // }); // it("should include enough gas for both when borrowing", async () => { // const troves = await liquity.getTroves({ first: 10, sortedBy: "ascendingCollateralRatio" }); // const trove = await liquity.getTrove(); // const newTrove = troveWithICRBetween(troves[1], troves[2]); // // Make sure we're borrowing // const adjustment = trove.adjustTo(newTrove); // expect(adjustment.borrowZUSD).to.not.be.undefined; // const tx = await liquity.populate.adjustTrove(adjustment); // const originalGasEstimate = await provider.estimateGas(tx.rawPopulatedTransaction); // // A terribly rude user interferes again // await openTroves([rudeUser], [Trove.recreate(newTrove.addDebt(1))]); // // On top of that, we'll need to update lastFeeOperationTime // await increaseTime(120); // const newGasEstimate = await provider.estimateGas(tx.rawPopulatedTransaction); // const gasIncrease = newGasEstimate.sub(originalGasEstimate).toNumber(); // await waitForSuccess(tx.send()); // expect(gasIncrease).to.be.within(15000, 35000); // }); // }); // describe("Gas estimation (ZERO issuance)", () => { // const estimate = (tx: PopulatedEthersLiquityTransaction) => // provider.estimateGas(tx.rawPopulatedTransaction); // before(async function () { // if (network.name !== "hardhat") { // this.skip(); // } // deployment = await deployLiquity(deployer,undefined,undefined,undefined,undefined,undefined,undefined,undefined,undefined,undefined,undefined,true); // [deployerLiquity, liquity] = await connectUsers([deployer, user]); // }); // it("should include enough gas for issuing ZERO", async function () { // this.timeout("2m"); // if (deployment.presaleAddress) { // const presale = new ethers.Contract( // deployment.presaleAddress, // mockBalanceRedirectPresaleAbi, // provider // ); // await presale.connect(deployer).closePresale(); // } // await liquity.openTrove({ depositCollateral: 40, borrowZUSD: 4000 }); // await liquity.depositZUSDInStabilityPool(19); // await increaseTime(60); // // This will issue ZERO for the first time ever. That uses a whole lotta gas, and we don't // // want to pack any extra gas to prepare for this case specifically, because it only happens // // once. // await liquity.withdrawGainsFromStabilityPool(); // const claim = await liquity.populate.withdrawGainsFromStabilityPool(); // const deposit = await liquity.populate.depositZUSDInStabilityPool(1); // const withdraw = await liquity.populate.withdrawZUSDFromStabilityPool(1); // for (let i = 0; i < 5; ++i) { // for (const tx of [claim, deposit, withdraw]) { // const gasLimit = tx.rawPopulatedTransaction.gasLimit?.toNumber(); // const requiredGas = (await estimate(tx)).toNumber(); // assertDefined(gasLimit); // expect(requiredGas).to.be.at.most(gasLimit); // } // await increaseTime(60); // } // await waitForSuccess(claim.send()); // const creation = Trove.recreate(new Trove(Decimal.from(11.1), Decimal.from(2000.1))); // await deployerLiquity.openTrove(creation); // await deployerLiquity.depositZUSDInStabilityPool(creation.borrowZUSD); // await deployerLiquity.setPrice(198); // const liquidateTarget = await liquity.populate.liquidate(await deployer.getAddress()); // const liquidateMultiple = await liquity.populate.liquidateUpTo(40); // for (let i = 0; i < 5; ++i) { // for (const tx of [liquidateTarget, liquidateMultiple]) { // const gasLimit = tx.rawPopulatedTransaction.gasLimit?.toNumber(); // const requiredGas = (await estimate(tx)).toNumber(); // assertDefined(gasLimit); // expect(requiredGas).to.be.at.most(gasLimit); // } // await increaseTime(60); // } // await waitForSuccess(liquidateMultiple.send()); // }); // }); // });