From c72a171ddb8b8ab74187be64ef1ef3a16b34cc80 Mon Sep 17 00:00:00 2001 From: Prateek Date: Mon, 12 May 2025 14:46:15 +0400 Subject: [PATCH 1/3] feat: max rate is used to pay for notice period --- contracts/enclaves/MarketV1.sol | 46 +++++++++++++++++++++------------ 1 file changed, 29 insertions(+), 17 deletions(-) diff --git a/contracts/enclaves/MarketV1.sol b/contracts/enclaves/MarketV1.sol index 18bf796..ad32ea6 100644 --- a/contracts/enclaves/MarketV1.sol +++ b/contracts/enclaves/MarketV1.sol @@ -181,6 +181,7 @@ contract MarketV1 is uint256 rate; uint256 balance; uint256 lastSettled; // payment has been settled up to this timestamp + uint256 maxRate; // max rate for the job } mapping(bytes32 => Job) public jobs; @@ -250,14 +251,16 @@ contract MarketV1 is function _emergencyWithdrawCredit(address _to, bytes32[] calldata _jobIds) internal { require(hasRole(EMERGENCY_WITHDRAW_ROLE, _to), "only to emergency withdraw role"); - uint256 settleTill = block.timestamp + noticePeriod; - for (uint256 i = 0; i < _jobIds.length; i++) { bytes32 jobId = _jobIds[i]; - _jobSettle(jobId, jobs[jobId].rate, settleTill); + _jobSettle(jobId, jobs[jobId].rate); uint256 creditBalance = jobCreditBalance[jobId]; if (creditBalance > 0) { - _withdraw(jobId, _to, creditBalance); + jobs[jobId].balance -= creditBalance; + // set job credit balance to 0 + jobCreditBalance[jobId] = 0; + creditToken.safeTransfer(_to, creditBalance); + emit JobWithdrawn(jobId, address(creditToken), _to, creditBalance); } } } @@ -274,7 +277,7 @@ contract MarketV1 is bytes32 jobId = bytes32(_jobIndex); // create job with initial balance 0 - jobs[jobId] = Job(_metadata, _owner, _provider, 0, 0, block.timestamp); + jobs[jobId] = Job(_metadata, _owner, _provider, 0, 0, block.timestamp, 0); emit JobOpened(jobId, _metadata, _owner, _provider, block.timestamp); // deposit initial balance @@ -284,25 +287,28 @@ contract MarketV1 is _jobReviseRate(jobId, _rate); } - function _jobSettle(bytes32 _jobId, uint256 _rate, uint256 _settleTill) internal returns (bool isBalanceEnough) { + function _jobSettle(bytes32 _jobId, uint256 _rate) internal returns (bool isBalanceEnough) { uint256 lastSettled = jobs[_jobId].lastSettled; - if (_settleTill == lastSettled) return true; - require(_settleTill > lastSettled, "cannot settle before lastSettled"); + if (block.timestamp <= lastSettled) { + return true; + } + require(jobs[_jobId].balance > 0, "insufficient funds to settle"); + require(jobs[_jobId].rate > 0, "invalid rate"); - uint256 usageDuration = _settleTill - lastSettled; + uint256 usageDuration = block.timestamp - lastSettled; uint256 amountUsed = _calcAmountUsed(_rate, usageDuration); uint256 settleAmount = _min(amountUsed, jobs[_jobId].balance); _settle(_jobId, settleAmount); - jobs[_jobId].lastSettled = _settleTill; - emit JobSettled(_jobId, _settleTill); + jobs[_jobId].lastSettled = block.timestamp; + emit JobSettled(_jobId, block.timestamp); isBalanceEnough = amountUsed <= settleAmount; } function _jobClose(bytes32 _jobId) internal { // deduct shutdown delay cost - _jobSettle(_jobId, jobs[_jobId].rate, block.timestamp + noticePeriod); + _jobSettle(_jobId, jobs[_jobId].rate); // refund leftover balance uint256 _balance = jobs[_jobId].balance; @@ -316,14 +322,14 @@ contract MarketV1 is function _jobDeposit(bytes32 _jobId, uint256 _amount) internal { require(_amount > 0, "invalid amount"); - require(_jobSettle(_jobId, jobs[_jobId].rate, block.timestamp + noticePeriod), "insufficient funds to deposit"); + require(_jobSettle(_jobId, jobs[_jobId].rate), "insufficient funds to deposit"); _deposit(_jobId, _msgSender(), _amount); } function _jobWithdraw(bytes32 _jobId, uint256 _amount) internal { require(_amount > 0, "invalid amount"); - require(_jobSettle(_jobId, jobs[_jobId].rate, block.timestamp + noticePeriod), "insufficient funds to withdraw"); + require(_jobSettle(_jobId, jobs[_jobId].rate), "insufficient funds to withdraw"); // withdraw _withdraw(_jobId, _msgSender(), _amount); @@ -336,7 +342,7 @@ contract MarketV1 is uint256 lastSettled = jobs[_jobId].lastSettled; if (block.timestamp > lastSettled) { require( - _jobSettle(_jobId, jobs[_jobId].rate, block.timestamp), + _jobSettle(_jobId, jobs[_jobId].rate), "insufficient funds to settle before revising rate" ); } @@ -349,7 +355,13 @@ contract MarketV1 is // deduct shutdown delay cost // higher rate is used to calculate shutdown delay cost uint256 higherRate = _max(oldRate, _newRate); - require(_jobSettle(_jobId, higherRate, block.timestamp + noticePeriod), "insufficient funds to revise rate"); + uint256 prevHighestRate = jobs[_jobId].maxRate; + if (higherRate > prevHighestRate) { + jobs[_jobId].maxRate = higherRate; + uint256 noticePeriodExtraCost = _calcAmountUsed((higherRate - prevHighestRate), noticePeriod); + require(jobs[_jobId].balance > noticePeriodExtraCost, "insufficient funds to revise rate"); + _settle(_jobId, noticePeriodExtraCost); + } } function _jobMetadataUpdate(bytes32 _jobId, string calldata _metadata) internal { @@ -388,7 +400,7 @@ contract MarketV1 is * @param _jobId The job to settle. */ function jobSettle(bytes32 _jobId) external onlyExistingJob(_jobId) { - _jobSettle(_jobId, jobs[_jobId].rate, block.timestamp); + _jobSettle(_jobId, jobs[_jobId].rate); } /** From 776f2bfc1917298026faf429d92d0335554bbbe0 Mon Sep 17 00:00:00 2001 From: Prateek Date: Mon, 12 May 2025 15:37:12 +0400 Subject: [PATCH 2/3] fix: tests for Market V1 --- test/enclaves/MarketV1.ts | 1585 +++++-------------------------------- 1 file changed, 197 insertions(+), 1388 deletions(-) diff --git a/test/enclaves/MarketV1.ts b/test/enclaves/MarketV1.ts index d42380d..50dfd51 100644 --- a/test/enclaves/MarketV1.ts +++ b/test/enclaves/MarketV1.ts @@ -312,8 +312,6 @@ describe("MarketV1", function () { let admin2: Signer; let INITIAL_JOB_INDEX: string; - let JOB_OPENED_TIMESTAMP: number; - before(async function () { signers = await ethers.getSigners(); @@ -475,7 +473,8 @@ describe("MarketV1", function () { expect(jobInfo.provider).to.equal(await provider.getAddress()); expect(jobInfo.rate).to.equal(JOB_RATE_1); expect(jobInfo.balance).to.equal(initialBalance.sub(noticePeriodCost)); - expect(jobInfo.lastSettled).to.be.within(INITIAL_TIMESTAMP + FIVE_MINUTES, INITIAL_TIMESTAMP + FIVE_MINUTES + 1); + expect(jobInfo.lastSettled).to.be.within(INITIAL_TIMESTAMP, INITIAL_TIMESTAMP + 1); + expect(jobInfo.maxRate).to.equal(JOB_RATE_1); expect(await token.balanceOf(await user.getAddress())).to.equal(SIGNER1_INITIAL_FUND.sub(initialBalance)); expect(await token.balanceOf(marketv1.address)).to.equal(initialBalance.sub(noticePeriodCost)); @@ -528,7 +527,7 @@ describe("MarketV1", function () { expect(jobInfo.provider).to.equal(await provider.getAddress()); expect(jobInfo.rate).to.equal(JOB_RATE_1); expect(jobInfo.balance).to.equal(initialBalance.sub(noticePeriodCost)); - expect(jobInfo.lastSettled).to.be.within(INITIAL_TIMESTAMP + FIVE_MINUTES, INITIAL_TIMESTAMP + FIVE_MINUTES + 1); + expect(jobInfo.lastSettled).to.be.within(INITIAL_TIMESTAMP, INITIAL_TIMESTAMP + 1); expect(await marketv1.jobCreditBalance(INITIAL_JOB_INDEX)).to.equal(initialBalance.sub(noticePeriodCost)); }); @@ -569,10 +568,7 @@ describe("MarketV1", function () { describe("Job Settle", function () { const initialDeposit = usdc(50); const initialBalance = initialDeposit.sub(calcNoticePeriodCost(JOB_RATE_1)); - - beforeEach(async () => { - JOB_OPENED_TIMESTAMP = (await ethers.provider.getBlock('latest')).timestamp; - }); + let jobOpenActualTimestamp: number; takeSnapshotBeforeAndAfterEveryTest(async () => { }); @@ -583,40 +579,55 @@ describe("MarketV1", function () { await marketv1 .connect(user) .jobOpen("some metadata", await provider.getAddress(), JOB_RATE_1, initialDeposit); + jobOpenActualTimestamp = (await ethers.provider.getBlock('latest')).timestamp; }); describe("CASE1: Settle Job immediately after Job Open", function () { - it("should revert before lastSettled", async () => { - await expect(marketv1.connect(user).jobSettle(INITIAL_JOB_INDEX)).to.be.revertedWith("cannot settle before lastSettled"); + it("should have balance and lastSettled reflecting initial state after open", async () => { + const jobBeforeSettle = await marketv1.jobs(INITIAL_JOB_INDEX); + await marketv1.connect(user).jobSettle(INITIAL_JOB_INDEX); + const jobInfo = await marketv1.jobs(INITIAL_JOB_INDEX); + + expect(jobInfo.balance).to.equal(initialBalance); + expect(jobInfo.lastSettled).to.be.closeTo(jobOpenActualTimestamp, 2); }); }); describe("CASE2: Settle Job 2 minutes after Job Open", function () { - it("should revert before lastSettled", async () => { - const TWO_MINUTES = 60 * 2; - await time.increaseTo(INITIAL_TIMESTAMP + TWO_MINUTES); + it("should settle for 2 minutes", async () => { + const DURATION_TO_SETTLE = TWO_MINUTES; + const expectedSettledTimestamp = jobOpenActualTimestamp + DURATION_TO_SETTLE; + await time.increaseTo(expectedSettledTimestamp); - await expect(marketv1.connect(user).jobSettle(INITIAL_JOB_INDEX)).to.be.revertedWith("cannot settle before lastSettled"); + await marketv1.connect(user).jobSettle(INITIAL_JOB_INDEX); + const jobInfo = await marketv1.jobs(INITIAL_JOB_INDEX); + + expect(jobInfo.lastSettled).to.be.closeTo(expectedSettledTimestamp, 2); + expect(jobInfo.balance).to.equal(initialBalance.sub(calcAmountToPay(JOB_RATE_1, DURATION_TO_SETTLE))); }); }); describe("CASE3: Settle Job 1 second before notice period", function () { - it("should revert before lastSettled", async () => { - const jobOpenedTimestamp = (await ethers.provider.getBlock('latest')).timestamp; - await time.increaseTo(jobOpenedTimestamp + NOTICE_PERIOD - 1); - - const lastSettled = (await marketv1.jobs(INITIAL_JOB_INDEX)).lastSettled; - expect(lastSettled).to.equal(jobOpenedTimestamp + NOTICE_PERIOD); - await expect(marketv1.connect(user).jobSettle(INITIAL_JOB_INDEX)).to.be.revertedWith("cannot settle before lastSettled"); + it("should settle for (notice period - 1 second)", async () => { + const DURATION_TO_SETTLE = NOTICE_PERIOD - 1; + const expectedSettledTimestamp = jobOpenActualTimestamp + DURATION_TO_SETTLE; + await time.increaseTo(expectedSettledTimestamp); + + await marketv1.connect(user).jobSettle(INITIAL_JOB_INDEX); + const jobInfo = await marketv1.jobs(INITIAL_JOB_INDEX); + + expect(jobInfo.lastSettled).to.be.closeTo(expectedSettledTimestamp, 2); + expect(jobInfo.balance).to.equal(initialBalance.sub(calcAmountToPay(JOB_RATE_1, DURATION_TO_SETTLE))); }); }); describe("CASE4: Settle Job 2 minutes after notice period", function () { - it("should spend notice period cost and 2 minutes worth tokens", async () => { - const TWO_MINUTES = 60 * 2; - await time.increaseTo(INITIAL_TIMESTAMP + NOTICE_PERIOD + TWO_MINUTES); + it("should spend for (notice period + 2 minutes)", async () => { + const DURATION_TO_SETTLE = NOTICE_PERIOD + TWO_MINUTES; + const expectedSettledTimestamp = jobOpenActualTimestamp + DURATION_TO_SETTLE; + await time.increaseTo(expectedSettledTimestamp); - // Job Settle + const noticeCostPaidAtOpen = calcNoticePeriodCost(JOB_RATE_1); await marketv1.connect(user).jobSettle(INITIAL_JOB_INDEX); const jobInfo = await marketv1.jobs(INITIAL_JOB_INDEX); @@ -625,95 +636,112 @@ describe("MarketV1", function () { expect(jobInfo.provider).to.equal(await provider.getAddress()); expect(jobInfo.rate).to.equal(JOB_RATE_1); - const jobBalanceExpected = initialBalance.sub(calcAmountToPay(JOB_RATE_1, TWO_MINUTES)); - expect(jobInfo.balance).to.be.within(jobBalanceExpected.sub(JOB_RATE_1), jobBalanceExpected.add(JOB_RATE_1)); + const amountPaidThisSettle = calcAmountToPay(JOB_RATE_1, DURATION_TO_SETTLE); + const jobBalanceExpected = initialBalance.sub(amountPaidThisSettle); + expect(jobInfo.balance).to.be.closeTo(jobBalanceExpected, 2); - const lastSettledTimestampExpected = INITIAL_TIMESTAMP + NOTICE_PERIOD + TWO_MINUTES; - expect(jobInfo.lastSettled).to.equal(lastSettledTimestampExpected); + expect(jobInfo.lastSettled).to.be.closeTo(expectedSettledTimestamp, 2); - // User balance const userBalanceExpected = SIGNER1_INITIAL_FUND.sub(initialDeposit); expect(await token.balanceOf(await user.getAddress())).to.equal(userBalanceExpected); - // Provider balance - const providerBalanceExpected = calcAmountToPay(JOB_RATE_1, TWO_MINUTES).add(calcNoticePeriodCost(JOB_RATE_1)); - expect(await token.balanceOf(await provider.getAddress())).to.be.within(providerBalanceExpected.sub(JOB_RATE_1), providerBalanceExpected.add(JOB_RATE_1)); + const providerBalanceExpected = noticeCostPaidAtOpen.add(amountPaidThisSettle); + expect(await token.balanceOf(await provider.getAddress())).to.be.closeTo(providerBalanceExpected, 2); - // MarketV1 balance - const marketv1BalanceExpected = initialBalance.sub(calcAmountToPay(JOB_RATE_1, TWO_MINUTES)); - expect(await token.balanceOf(marketv1.address)).to.be.within(marketv1BalanceExpected.sub(JOB_RATE_1), marketv1BalanceExpected.add(JOB_RATE_1)); + expect(await token.balanceOf(marketv1.address)).to.be.closeTo(jobBalanceExpected, 2); }); }); }); describe("Credit Only", function () { beforeEach(async () => { - // await token.connect(user).approve(marketv1.address, initialDeposit); await creditToken.connect(user).approve(marketv1.address, initialDeposit); await marketv1 .connect(user) .jobOpen("some metadata", await provider.getAddress(), JOB_RATE_1, initialDeposit); + jobOpenActualTimestamp = (await ethers.provider.getBlock('latest')).timestamp; }); describe("CASE1: Settle Job immediately after Job Open", function () { - it("should revert before lastSettled", async () => { - await expect(marketv1.connect(user).jobSettle(INITIAL_JOB_INDEX)).to.be.revertedWith("cannot settle before lastSettled"); + it("should have balance and lastSettled reflecting initial state after open", async () => { + const jobBeforeSettle = await marketv1.jobs(INITIAL_JOB_INDEX); + const creditBalanceBeforeSettle = await marketv1.jobCreditBalance(INITIAL_JOB_INDEX); + + await marketv1.connect(user).jobSettle(INITIAL_JOB_INDEX); + const jobInfo = await marketv1.jobs(INITIAL_JOB_INDEX); + const creditBalanceAfterSettle = await marketv1.jobCreditBalance(INITIAL_JOB_INDEX); + + expect(jobInfo.balance).to.equal(initialBalance); + expect(jobInfo.lastSettled).to.be.closeTo(jobOpenActualTimestamp, 2); + expect(creditBalanceAfterSettle).to.equal(creditBalanceBeforeSettle); + expect(creditBalanceAfterSettle).to.equal(initialBalance); }); }); describe("CASE2: Settle Job 2 minutes after Job Open", function () { - it("should revert before lastSettled", async () => { - const TWO_MINUTES = 60 * 2; - await time.increaseTo(INITIAL_TIMESTAMP + TWO_MINUTES); + it("should settle for 2 minutes using credit", async () => { + const DURATION_TO_SETTLE = TWO_MINUTES; + const expectedSettledTimestamp = jobOpenActualTimestamp + DURATION_TO_SETTLE; + await time.increaseTo(expectedSettledTimestamp); + + const amountToSettle = calcAmountToPay(JOB_RATE_1, DURATION_TO_SETTLE); - await expect(marketv1.connect(user).jobSettle(INITIAL_JOB_INDEX)).to.be.revertedWith("cannot settle before lastSettled"); + await marketv1.connect(user).jobSettle(INITIAL_JOB_INDEX); + const jobInfo = await marketv1.jobs(INITIAL_JOB_INDEX); + const jobCreditBal = await marketv1.jobCreditBalance(INITIAL_JOB_INDEX); + + expect(jobInfo.lastSettled).to.be.closeTo(expectedSettledTimestamp, 2); + expect(jobInfo.balance).to.equal(initialBalance.sub(amountToSettle)); + expect(jobCreditBal).to.equal(initialBalance.sub(amountToSettle)); }); }); describe("CASE3: Settle Job exactly after notice period", function () { - it("should revert before lastSettled", async () => { - await time.increaseTo(INITIAL_TIMESTAMP + NOTICE_PERIOD); + it("should settle for notice period duration using credit", async () => { + const DURATION_TO_SETTLE = NOTICE_PERIOD; + const expectedSettledTimestamp = jobOpenActualTimestamp + DURATION_TO_SETTLE; + await time.increaseTo(expectedSettledTimestamp); + + const amountToSettle = calcAmountToPay(JOB_RATE_1, DURATION_TO_SETTLE); - await expect(marketv1.connect(user).jobSettle(INITIAL_JOB_INDEX)).to.be.revertedWith("cannot settle before lastSettled"); + await marketv1.connect(user).jobSettle(INITIAL_JOB_INDEX); + const jobInfo = await marketv1.jobs(INITIAL_JOB_INDEX); + const jobCreditBal = await marketv1.jobCreditBalance(INITIAL_JOB_INDEX); + + expect(jobInfo.lastSettled).to.be.closeTo(expectedSettledTimestamp, 2); + expect(jobInfo.balance).to.equal(initialBalance.sub(amountToSettle)); + expect(jobCreditBal).to.equal(initialBalance.sub(amountToSettle)); }); }); describe("CASE4: Settle Job 2 minutes after notice period", function () { - it("should spend notice period cost and 2 minutes worth tokens", async () => { - const TWO_MINUTES = 60 * 2; - await time.increaseTo(INITIAL_TIMESTAMP + NOTICE_PERIOD + TWO_MINUTES); + it("should spend notice period cost and 2 minutes worth tokens from credit", async () => { + const DURATION_TO_SETTLE = NOTICE_PERIOD + TWO_MINUTES; + const expectedSettledTimestamp = jobOpenActualTimestamp + DURATION_TO_SETTLE; + await time.increaseTo(expectedSettledTimestamp); - // Job Settle + const noticeCostPaidAtOpen = calcNoticePeriodCost(JOB_RATE_1); await marketv1.connect(user).jobSettle(INITIAL_JOB_INDEX); - // Job Info After Settle const jobInfo = await marketv1.jobs(INITIAL_JOB_INDEX); + const jobCreditBal = await marketv1.jobCreditBalance(INITIAL_JOB_INDEX); expect(jobInfo.metadata).to.equal("some metadata"); expect(jobInfo.owner).to.equal(await user.getAddress()); expect(jobInfo.provider).to.equal(await provider.getAddress()); expect(jobInfo.rate).to.equal(JOB_RATE_1); - const jobBalanceExpected = initialBalance.sub(calcAmountToPay(JOB_RATE_1, TWO_MINUTES)); - expect(jobInfo.balance).to.be.within(jobBalanceExpected.sub(JOB_RATE_1), jobBalanceExpected.add(JOB_RATE_1)); + const amountPaidThisSettle = calcAmountToPay(JOB_RATE_1, DURATION_TO_SETTLE); + const jobBalanceExpected = initialBalance.sub(amountPaidThisSettle); + expect(jobInfo.balance).to.be.closeTo(jobBalanceExpected, 2); + expect(jobCreditBal).to.be.closeTo(jobBalanceExpected, 2); - const lastSettledTimestampExpected = INITIAL_TIMESTAMP + NOTICE_PERIOD + TWO_MINUTES; - expect(jobInfo.lastSettled).to.be.within(lastSettledTimestampExpected - 3, lastSettledTimestampExpected + 3); + expect(jobInfo.lastSettled).to.be.closeTo(expectedSettledTimestamp, 3); - // User balance - const userTokenBalanceExpected = SIGNER1_INITIAL_FUND; - expect(await token.balanceOf(await user.getAddress())).to.be.within(userTokenBalanceExpected.sub(JOB_RATE_1), userTokenBalanceExpected.add(JOB_RATE_1)); - const userCreditBalanceExpected = initialBalance.sub(calcAmountToPay(JOB_RATE_1, TWO_MINUTES)); - expect(await creditToken.balanceOf(await user.getAddress())).to.be.within(userCreditBalanceExpected.sub(JOB_RATE_1), userCreditBalanceExpected.add(JOB_RATE_1)); + const providerBalanceExpected = noticeCostPaidAtOpen.add(amountPaidThisSettle); + expect(await token.balanceOf(await provider.getAddress())).to.be.closeTo(providerBalanceExpected, 2); - // Provider balance - const providerBalanceExpected = calcAmountToPay(JOB_RATE_1, TWO_MINUTES).add(calcNoticePeriodCost(JOB_RATE_1)); - expect(await token.balanceOf(await provider.getAddress())).to.be.within(providerBalanceExpected.sub(JOB_RATE_1), providerBalanceExpected.add(JOB_RATE_1)); - - // MarketV1 balance - const marketv1TokenBalanceExpected = initialBalance.sub(calcAmountToPay(JOB_RATE_1, TWO_MINUTES)); - expect(await token.balanceOf(marketv1.address)).to.be.within(marketv1TokenBalanceExpected.sub(JOB_RATE_1), marketv1TokenBalanceExpected.add(JOB_RATE_1)); - const marketv1CreditBalanceExpected = calcAmountToPay(JOB_RATE_1, TWO_MINUTES); - expect(await creditToken.balanceOf(marketv1.address)).to.be.within(marketv1CreditBalanceExpected.sub(JOB_RATE_1), marketv1CreditBalanceExpected.add(JOB_RATE_1)); + expect(await token.balanceOf(marketv1.address)).to.equal(0); + expect(await creditToken.balanceOf(marketv1.address)).to.be.closeTo(jobCreditBal, 2); }); }); }); @@ -725,1367 +753,148 @@ describe("MarketV1", function () { beforeEach(async () => { await token.connect(user).approve(marketv1.address, usdcDeposit); await creditToken.connect(user).approve(marketv1.address, creditDeposit); - // deposit 10 credit and 40 usdc await marketv1 .connect(user) .jobOpen("some metadata", await provider.getAddress(), JOB_RATE_1, usdcDeposit.add(creditDeposit)); + jobOpenActualTimestamp = (await ethers.provider.getBlock('latest')).timestamp; }); describe("CASE1: Settle Job immediately after Job Open", function () { - it("should revert before lastSettled", async () => { - await expect(marketv1.connect(user).jobSettle(INITIAL_JOB_INDEX)).to.be.revertedWith("cannot settle before lastSettled"); + it("should have balance and lastSettled reflecting initial state after open", async () => { + const jobBeforeSettle = await marketv1.jobs(INITIAL_JOB_INDEX); + const creditBalanceBeforeSettle = await marketv1.jobCreditBalance(INITIAL_JOB_INDEX); + + await marketv1.connect(user).jobSettle(INITIAL_JOB_INDEX); + const jobInfo = await marketv1.jobs(INITIAL_JOB_INDEX); + const creditBalanceAfterSettle = await marketv1.jobCreditBalance(INITIAL_JOB_INDEX); + + expect(jobInfo.balance).to.equal(initialBalance); + expect(jobInfo.lastSettled).to.be.closeTo(jobOpenActualTimestamp, 2); + expect(creditBalanceAfterSettle).to.equal(creditBalanceBeforeSettle); + expect(creditBalanceAfterSettle).to.equal(creditDeposit.sub(calcNoticePeriodCost(JOB_RATE_1))); }); }); describe("CASE2: Settle Job 2 minutes after Job Open", function () { - it("should revert before lastSettled", async () => { - const TWO_MINUTES = 60 * 2; - await time.increaseTo(INITIAL_TIMESTAMP + TWO_MINUTES); - - await expect(marketv1.connect(user).jobSettle(INITIAL_JOB_INDEX)).to.be.revertedWith("cannot settle before lastSettled"); + it("should settle for 2 minutes, using credit then USDC", async () => { + const DURATION_TO_SETTLE = TWO_MINUTES; + const expectedSettledTimestamp = jobOpenActualTimestamp + DURATION_TO_SETTLE; + await time.increaseTo(expectedSettledTimestamp); + + const amountToSettle = calcAmountToPay(JOB_RATE_1, DURATION_TO_SETTLE); + const initialJobCreditBalance = creditDeposit.sub(calcNoticePeriodCost(JOB_RATE_1)); + + await marketv1.connect(user).jobSettle(INITIAL_JOB_INDEX); + const jobInfo = await marketv1.jobs(INITIAL_JOB_INDEX); + const jobCreditBal = await marketv1.jobCreditBalance(INITIAL_JOB_INDEX); + + expect(jobInfo.lastSettled).to.be.closeTo(expectedSettledTimestamp, 2); + expect(jobInfo.balance).to.equal(initialBalance.sub(amountToSettle)); + + let expectedCreditBalanceAfterSettle = initialJobCreditBalance.sub(amountToSettle); + if (expectedCreditBalanceAfterSettle.lt(0)) { + expectedCreditBalanceAfterSettle = BN.from(0); + } + expect(jobCreditBal).to.equal(expectedCreditBalanceAfterSettle); }); }); describe("CASE3: Settle Job exactly after notice period", function () { - it("should revert before lastSettled", async () => { - await time.increaseTo(INITIAL_TIMESTAMP + NOTICE_PERIOD); - - await expect(marketv1.connect(user).jobSettle(INITIAL_JOB_INDEX)).to.be.revertedWith("cannot settle before lastSettled"); + it("should settle for notice period, using credit then USDC", async () => { + const DURATION_TO_SETTLE = NOTICE_PERIOD; + const expectedSettledTimestamp = jobOpenActualTimestamp + DURATION_TO_SETTLE; + await time.increaseTo(expectedSettledTimestamp); + + const amountToSettle = calcAmountToPay(JOB_RATE_1, DURATION_TO_SETTLE); + const initialJobCreditBalance = creditDeposit.sub(calcNoticePeriodCost(JOB_RATE_1)); + + await marketv1.connect(user).jobSettle(INITIAL_JOB_INDEX); + const jobInfo = await marketv1.jobs(INITIAL_JOB_INDEX); + const jobCreditBal = await marketv1.jobCreditBalance(INITIAL_JOB_INDEX); + + expect(jobInfo.lastSettled).to.be.closeTo(expectedSettledTimestamp, 2); + expect(jobInfo.balance).to.equal(initialBalance.sub(amountToSettle)); + + let expectedCreditBalanceAfterSettle = initialJobCreditBalance.sub(amountToSettle); + if (expectedCreditBalanceAfterSettle.lt(0)) { + expectedCreditBalanceAfterSettle = BN.from(0); + } + expect(jobCreditBal).to.equal(expectedCreditBalanceAfterSettle); }); }); - describe("CASE4: Settle Job 2 minutes after notice period - only Credit is settled", function () { - it("should settle notice period cost and 2 minutes worth tokens only from Credit", async () => { - const TWO_MINUTES = 60 * 2; - const TIME_JOB_OPEN = (await ethers.provider.getBlock('latest')).timestamp; - await time.increaseTo(TIME_JOB_OPEN + NOTICE_PERIOD + TWO_MINUTES); - const TIME_JOB_SETTLE = (await ethers.provider.getBlock('latest')).timestamp; - const TIME_DIFF = TIME_JOB_SETTLE - TIME_JOB_OPEN; - - const noticePeriodCost = calcNoticePeriodCost(JOB_RATE_1); + describe("CASE4: Settle Job 2 minutes after notice period - credit might be used up", function () { + it("should settle, exhausting credit first, then USDC", async () => { + const DURATION_TO_SETTLE = NOTICE_PERIOD + TWO_MINUTES; + const TIME_JOB_SETTLE_TARGET = jobOpenActualTimestamp + DURATION_TO_SETTLE; + await time.increaseTo(TIME_JOB_SETTLE_TARGET); + + const noticeCostPaidAtOpen = calcNoticePeriodCost(JOB_RATE_1); + const initialJobCreditBalance = creditDeposit.sub(noticeCostPaidAtOpen); - // Job Settle await marketv1.connect(user).jobSettle(INITIAL_JOB_INDEX); - /* Job Info After Settle */ const jobInfo = await marketv1.jobs(INITIAL_JOB_INDEX); + const finalJobCreditBalance = await marketv1.jobCreditBalance(INITIAL_JOB_INDEX); + expect(jobInfo.metadata).to.equal("some metadata"); expect(jobInfo.owner).to.equal(await user.getAddress()); expect(jobInfo.provider).to.equal(await provider.getAddress()); expect(jobInfo.rate).to.equal(JOB_RATE_1); - // Job Balance - const jobBalanceExpected = initialBalance.sub(calcAmountToPay(JOB_RATE_1, TWO_MINUTES)); - expect(jobInfo.balance).to.be.within(jobBalanceExpected.sub(JOB_RATE_1), jobBalanceExpected.add(JOB_RATE_1)); - // Job Last Settled - const lastSettledTimestampExpected = TIME_JOB_OPEN + NOTICE_PERIOD + TWO_MINUTES; - expect(jobInfo.lastSettled).to.be.within(lastSettledTimestampExpected - 3, lastSettledTimestampExpected + 3); - // Job Credit Balance - const amountSettledExpected = calcAmountToPay(JOB_RATE_1, TIME_DIFF); - const jobCreditBalanceExpected = creditDeposit.sub(amountSettledExpected); - expect(await marketv1.jobCreditBalance(INITIAL_JOB_INDEX)).to.be.within(jobCreditBalanceExpected.sub(JOB_RATE_1), jobCreditBalanceExpected.add(JOB_RATE_1)); - - /* User balance */ - // User Token balance - const userTokenBalanceExpected = SIGNER1_INITIAL_FUND.sub(usdcDeposit); - expect(await token.balanceOf(await user.getAddress())).to.be.within(userTokenBalanceExpected.sub(JOB_RATE_1), userTokenBalanceExpected.add(JOB_RATE_1)); - // User Credit balance - const userCreditBalanceExpected = initialBalance.sub(calcAmountToPay(JOB_RATE_1, TWO_MINUTES)); - expect(await creditToken.balanceOf(await user.getAddress())).to.be.within(userCreditBalanceExpected.sub(JOB_RATE_1), userCreditBalanceExpected.add(JOB_RATE_1)); - - /* Provider balance */ - // Provider Token balance - const providerBalanceExpected = calcAmountToPay(JOB_RATE_1, TWO_MINUTES).add(calcNoticePeriodCost(JOB_RATE_1)); - expect(await token.balanceOf(await provider.getAddress())).to.be.within(providerBalanceExpected.sub(JOB_RATE_1), providerBalanceExpected.add(JOB_RATE_1)); - - /* MarketV1 balance */ - // MarketV1 Token balance - const marketv1TokenBalanceExpected = initialBalance.sub(calcAmountToPay(JOB_RATE_1, TWO_MINUTES)); - expect(await token.balanceOf(marketv1.address)).to.be.within(marketv1TokenBalanceExpected.sub(JOB_RATE_1), marketv1TokenBalanceExpected.add(JOB_RATE_1)); - // MarketV1 Credit balance - const marketv1CreditBalanceExpected = calcAmountToPay(JOB_RATE_1, TWO_MINUTES); - expect(await creditToken.balanceOf(marketv1.address)).to.be.within(marketv1CreditBalanceExpected.sub(JOB_RATE_1), marketv1CreditBalanceExpected.add(JOB_RATE_1)); + + const amountPaidThisSettle = calcAmountToPay(JOB_RATE_1, DURATION_TO_SETTLE); + const jobBalanceExpected = initialBalance.sub(amountPaidThisSettle); + expect(jobInfo.balance).to.be.closeTo(jobBalanceExpected, 2); + + expect(jobInfo.lastSettled).to.be.closeTo(TIME_JOB_SETTLE_TARGET, 3); + + let expectedFinalCreditBalance = initialJobCreditBalance.sub(amountPaidThisSettle); + if (expectedFinalCreditBalance.lt(0)) { + expectedFinalCreditBalance = BN.from(0); + } + expect(finalJobCreditBalance).to.equal(expectedFinalCreditBalance); + + const providerPaymentThisSettle = amountPaidThisSettle; + const providerBalanceExpected = noticeCostPaidAtOpen.add(providerPaymentThisSettle); + expect(await token.balanceOf(await provider.getAddress())).to.be.closeTo(providerBalanceExpected, 2); + + const expectedMarketV1UsdcBalance = jobBalanceExpected.sub(expectedFinalCreditBalance); + expect(await token.balanceOf(marketv1.address)).to.be.closeTo(expectedMarketV1UsdcBalance, 2); + expect(await creditToken.balanceOf(marketv1.address)).to.equal(expectedFinalCreditBalance); }); }); describe("CASE5: Settle Job 20 minutes after notice period - both Credit and USDC are settled", function () { it("should settle all Credits and some USDC", async () => { - const TWENTY_MINUTES = 60 * 20; - await time.increaseTo(JOB_OPENED_TIMESTAMP + NOTICE_PERIOD + TWENTY_MINUTES); - const TIME_JOB_SETTLE = (await ethers.provider.getBlock('latest')).timestamp; - const TIME_DIFF = TIME_JOB_SETTLE - JOB_OPENED_TIMESTAMP; + const DURATION_TO_SETTLE = NOTICE_PERIOD + 60 * 20; + const TIME_JOB_SETTLE_TARGET = jobOpenActualTimestamp + DURATION_TO_SETTLE; + await time.increaseTo(TIME_JOB_SETTLE_TARGET); - const noticePeriodCost = calcNoticePeriodCost(JOB_RATE_1); + const noticeCostPaidAtOpen = calcNoticePeriodCost(JOB_RATE_1); + const initialJobCreditBalance = creditDeposit.sub(noticeCostPaidAtOpen); - // Job Settle await marketv1.connect(user).jobSettle(INITIAL_JOB_INDEX); - /* Job Info After Settle */ const jobInfo = await marketv1.jobs(INITIAL_JOB_INDEX); + const finalJobCreditBalance = await marketv1.jobCreditBalance(INITIAL_JOB_INDEX); expect(jobInfo.metadata).to.equal("some metadata"); - expect(jobInfo.owner).to.equal(await user.getAddress()); - expect(jobInfo.provider).to.equal(await provider.getAddress()); - expect(jobInfo.rate).to.equal(JOB_RATE_1); - // Job Balance - const jobBalanceExpected = initialBalance.sub(calcAmountToPay(JOB_RATE_1, TWENTY_MINUTES)); - expect(jobInfo.balance).to.be.within(jobBalanceExpected.sub(JOB_RATE_1), jobBalanceExpected.add(JOB_RATE_1)); - // Job Last Settled - const lastSettledTimestampExpected = JOB_OPENED_TIMESTAMP + NOTICE_PERIOD + TWENTY_MINUTES; - expect(jobInfo.lastSettled).to.be.within(lastSettledTimestampExpected - 3, lastSettledTimestampExpected + 3); - // Job Credit Balance - const amountSettledExpected = calcAmountToPay(JOB_RATE_1, TIME_DIFF); - const jobCreditBalanceExpected = creditDeposit.sub(amountSettledExpected); - expect(await marketv1.jobCreditBalance(INITIAL_JOB_INDEX)).to.be.within(jobCreditBalanceExpected.sub(JOB_RATE_1), jobCreditBalanceExpected.add(JOB_RATE_1)); - expect(await marketv1.jobCreditBalance(INITIAL_JOB_INDEX)).to.equal(0); - - /* User balance */ - // User Token balance - const userTokenBalanceExpected = SIGNER1_INITIAL_FUND.sub(usdcDeposit); - expect(await token.balanceOf(await user.getAddress())).to.be.within(userTokenBalanceExpected.sub(JOB_RATE_1), userTokenBalanceExpected.add(JOB_RATE_1)); - // User Credit balance - const userCreditBalanceExpected = initialBalance.sub(calcAmountToPay(JOB_RATE_1, TWO_MINUTES)); - expect(await creditToken.balanceOf(await user.getAddress())).to.be.within(userCreditBalanceExpected.sub(JOB_RATE_1), userCreditBalanceExpected.add(JOB_RATE_1)); - - /* Provider balance */ - // Provider Token balance - const providerBalanceExpected = calcAmountToPay(JOB_RATE_1, TWO_MINUTES).add(calcNoticePeriodCost(JOB_RATE_1)); - expect(await token.balanceOf(await provider.getAddress())).to.be.within(providerBalanceExpected.sub(JOB_RATE_1), providerBalanceExpected.add(JOB_RATE_1)); - - /* MarketV1 balance */ - // MarketV1 Token balance - const marketv1TokenBalanceExpected = initialBalance.sub(calcAmountToPay(JOB_RATE_1, TWO_MINUTES)); - expect(await token.balanceOf(marketv1.address)).to.be.within(marketv1TokenBalanceExpected.sub(JOB_RATE_1), marketv1TokenBalanceExpected.add(JOB_RATE_1)); - // MarketV1 Credit balance - const marketv1CreditBalanceExpected = calcAmountToPay(JOB_RATE_1, TWO_MINUTES); - expect(await creditToken.balanceOf(marketv1.address)).to.be.within(marketv1CreditBalanceExpected.sub(JOB_RATE_1), marketv1CreditBalanceExpected.add(JOB_RATE_1)); - }); - }); - }); - }); - - describe("Job Deposit", function () { - takeSnapshotBeforeAndAfterEveryTest(async () => { }); - - describe("USDC Only", function () { - it("should deposit to job with USDC", async () => { - const initialDeposit = usdc(50); - const initialBalance = initialDeposit.sub(calcNoticePeriodCost(JOB_RATE_1)); - const additionalDepositAmount = usdc(25); - - await marketv1 - .connect(user) - .jobOpen("some metadata", await provider.getAddress(), JOB_RATE_1, initialDeposit); - - // Deposit 25 USDC - await marketv1 - .connect(signers[1]) - .jobDeposit(INITIAL_JOB_INDEX, additionalDepositAmount); - - // Job after deposit - const jobInfo = await marketv1.jobs(INITIAL_JOB_INDEX); - expect(jobInfo.metadata).to.equal("some metadata"); - expect(jobInfo.owner).to.equal(addrs[1]); - expect(jobInfo.provider).to.equal(addrs[2]); - expect(jobInfo.rate).to.equal(JOB_RATE_1); - expect(jobInfo.balance).to.equal(initialBalance.add(additionalDepositAmount)); - expect(jobInfo.lastSettled).to.be.within(INITIAL_TIMESTAMP + FIVE_MINUTES - 3, INITIAL_TIMESTAMP + FIVE_MINUTES + 3); - - const userBalanceExpected = SIGNER1_INITIAL_FUND.sub(initialDeposit).sub(additionalDepositAmount); - expect(await token.balanceOf(addrs[1])).to.be.within(userBalanceExpected.sub(JOB_RATE_1), userBalanceExpected.add(JOB_RATE_1)); - - const marketv1BalanceExpected = initialBalance.add(additionalDepositAmount); - expect(await token.balanceOf(marketv1.address)).to.be.within(marketv1BalanceExpected.sub(JOB_RATE_1), marketv1BalanceExpected.add(JOB_RATE_1)); - }); - - it("should revert when depositing to job without enough approved", async () => { - const initialDeposit = usdc(50); - const initialBalance = initialDeposit.sub(calcNoticePeriodCost(JOB_RATE_1)); - const additionalDepositAmount = usdc(25); - - await token.connect(user).approve(marketv1.address, initialDeposit); - await marketv1 - .connect(user) - .jobOpen("some metadata", await provider.getAddress(), JOB_RATE_1, initialDeposit); - - // Deposit 25 USDC - await expect(marketv1 - .connect(user) - .jobDeposit(INITIAL_JOB_INDEX, additionalDepositAmount)).to.be.revertedWith("ERC20: insufficient allowance"); - }); - - it("should revert when depositing to job without enough balance", async () => { - const initialDeposit = SIGNER1_INITIAL_FUND; - const initialBalance = initialDeposit.sub(calcNoticePeriodCost(JOB_RATE_1)); - const additionalDepositAmount = usdc(25); - - // Open Job - await token.connect(user).approve(marketv1.address, SIGNER1_INITIAL_FUND.add(usdc(1000))); - await marketv1 - .connect(user) - .jobOpen("some metadata", await provider.getAddress(), JOB_RATE_1, initialDeposit); - - // Deposit 25 USDC - await expect(marketv1 - .connect(user) - .jobDeposit(INITIAL_JOB_INDEX, additionalDepositAmount)).to.be.revertedWith("ERC20: transfer amount exceeds balance"); - }); - - it("should revert when depositing to never registered job", async () => { - await expect(marketv1 - .connect(user) - .jobDeposit(ethers.utils.hexZeroPad("0x01", 32), 25)).to.be.revertedWith("job not found"); - }); - - it("should revert when depositing to closed job", async () => { - const initialDeposit = usdc(50); - - // Job Open - await token.connect(user).approve(marketv1.address, initialDeposit); - await marketv1 - .connect(user) - .jobOpen("some metadata", await provider.getAddress(), JOB_RATE_1, initialDeposit); - - // Job Close - await marketv1.connect(user).jobClose(INITIAL_JOB_INDEX); - - // Job Deposit - await expect(marketv1 - .connect(signers[1]) - .jobDeposit(INITIAL_JOB_INDEX, 25)).to.be.revertedWith("job not found"); - }); - }); - - describe("Credit Only", function () { - const INITIAL_DEPOSIT_AMOUNT = usdc(50); - const ADDITIONAL_DEPOSIT_AMOUNT = usdc(25); - const NOTICE_PERIOD_COST = calcNoticePeriodCost(JOB_RATE_1); - - it("should deposit to job with Credit", async () => { - await marketv1 - .connect(user) - .jobOpen("some metadata", await provider.getAddress(), JOB_RATE_1, INITIAL_DEPOSIT_AMOUNT); - - // Deposit 25 Credit - await creditToken.connect(user).approve(marketv1.address, ADDITIONAL_DEPOSIT_AMOUNT); - await marketv1 - .connect(signers[1]) - .jobDeposit(INITIAL_JOB_INDEX, ADDITIONAL_DEPOSIT_AMOUNT); - - const currentTimestamp = (await ethers.provider.getBlock('latest')).timestamp; - - // Job after deposit - const jobInfo = await marketv1.jobs(INITIAL_JOB_INDEX); - expect(jobInfo.metadata).to.equal("some metadata"); - expect(jobInfo.owner).to.equal(await user.getAddress()); - expect(jobInfo.provider).to.equal(await provider.getAddress()); - expect(jobInfo.rate).to.equal(JOB_RATE_1); - expect(jobInfo.balance).to.equal(INITIAL_DEPOSIT_AMOUNT.sub(NOTICE_PERIOD_COST).add(ADDITIONAL_DEPOSIT_AMOUNT)); - expect(jobInfo.lastSettled).to.equal(currentTimestamp + NOTICE_PERIOD); - - const userBalanceExpected = SIGNER1_INITIAL_FUND.sub(INITIAL_DEPOSIT_AMOUNT).sub(ADDITIONAL_DEPOSIT_AMOUNT); - expect(await token.balanceOf(addrs[1])).to.be.within(userBalanceExpected.sub(JOB_RATE_1), userBalanceExpected.add(JOB_RATE_1)); - - const marketv1BalanceExpected = INITIAL_DEPOSIT_AMOUNT.add(ADDITIONAL_DEPOSIT_AMOUNT); - expect(await token.balanceOf(marketv1.address)).to.be.within(marketv1BalanceExpected.sub(JOB_RATE_1), marketv1BalanceExpected.add(JOB_RATE_1)); - }); - - it("should revert when depositing to job without approving both Credit and USDC", async () => { - const additionalDepositAmount = usdc(25); - - await token.connect(user).approve(marketv1.address, INITIAL_DEPOSIT_AMOUNT); - await marketv1 - .connect(user) - .jobOpen("some metadata", await provider.getAddress(), JOB_RATE_1, INITIAL_DEPOSIT_AMOUNT); - - // Deposit without approving Credit - await expect(marketv1 - .connect(user) - .jobDeposit(INITIAL_JOB_INDEX, ADDITIONAL_DEPOSIT_AMOUNT)).to.be.revertedWith("ERC20: insufficient allowance"); - }); - - it("should deposit USDC when Credit credit balance is not enough", async () => { - const initialDeposit = SIGNER1_INITIAL_FUND; - - // Open Job - await token.connect(user2).approve(marketv1.address, SIGNER1_INITIAL_FUND.add(usdc(1000))); - await marketv1 - .connect(user2) - .jobOpen("some metadata", await provider.getAddress(), JOB_RATE_1, initialDeposit); - - // Approve 25 Credit without having enough Credit balance - await creditToken.connect(user2).approve(marketv1.address, ADDITIONAL_DEPOSIT_AMOUNT); - await expect(marketv1 - .connect(user2) - .jobDeposit(INITIAL_JOB_INDEX, ADDITIONAL_DEPOSIT_AMOUNT)).to.be.revertedWith("ERC20: transfer amount exceeds balance"); - - const jobInfo = await marketv1.jobs(INITIAL_JOB_INDEX); - const creditBalance = await creditToken.balanceOf(await user2.getAddress()); - expect(jobInfo.balance).to.equal(initialDeposit.sub(NOTICE_PERIOD_COST)); - expect(creditBalance).to.equal(0); - }); - - it("should revert when depositing to never registered job", async () => { - await creditToken.connect(user).approve(marketv1.address, INITIAL_DEPOSIT_AMOUNT); - await expect(marketv1 - .connect(user) - .jobDeposit(INITIAL_JOB_INDEX, ADDITIONAL_DEPOSIT_AMOUNT)).to.be.revertedWith("job not found"); - }); - - it("should revert when depositing to closed job", async () => { - const initialDeposit = usdc(50); - - // Job Open - await token.connect(user).approve(marketv1.address, initialDeposit); - await marketv1 - .connect(user) - .jobOpen("some metadata", await provider.getAddress(), JOB_RATE_1, initialDeposit); - - // Job Close - await marketv1.connect(user).jobClose(INITIAL_JOB_INDEX); - - // Job Deposit - await creditToken.connect(user).approve(marketv1.address, ADDITIONAL_DEPOSIT_AMOUNT); - await expect(marketv1 - .connect(signers[1]) - .jobDeposit(INITIAL_JOB_INDEX, 25)).to.be.revertedWith("job not found"); - }); - }); - - describe("Both Credit and USDC", function () { - const INITIAL_DEPOSIT_AMOUNT = usdc(50); - const NOTICE_PERIOD_COST = calcNoticePeriodCost(JOB_RATE_1); - const TOTAL_ADITIONAL_DEPOSIT_AMOUNT = usdc(40); - const ADDITIONAL_CREDIT_DEPOSIT_AMOUNT = usdc(30); - const ADDITIONAL_USDC_DEPOSIT_AMOUNT = usdc(10); - const TOTAL_DEPOSIT_AMOUNT = INITIAL_DEPOSIT_AMOUNT.add(ADDITIONAL_USDC_DEPOSIT_AMOUNT).add(ADDITIONAL_CREDIT_DEPOSIT_AMOUNT); - - it("should deposit 10 USDC and 30 Credit", async () => { - await marketv1 - .connect(user) - .jobOpen("some metadata", await provider.getAddress(), JOB_RATE_1, INITIAL_DEPOSIT_AMOUNT); - expect((await marketv1.jobs(INITIAL_JOB_INDEX)).balance).to.equal(INITIAL_DEPOSIT_AMOUNT.sub(NOTICE_PERIOD_COST)); - - // Deposit 30 Credit and 10 USDC - await creditToken.connect(user).approve(marketv1.address, ADDITIONAL_CREDIT_DEPOSIT_AMOUNT); - await marketv1 - .connect(user) - .jobDeposit(INITIAL_JOB_INDEX, TOTAL_ADITIONAL_DEPOSIT_AMOUNT); - - const currentTimestamp = (await ethers.provider.getBlock('latest')).timestamp; - - // Job after deposit - const jobInfo = await marketv1.jobs(INITIAL_JOB_INDEX); - const creditBalance = await marketv1.jobCreditBalance(INITIAL_JOB_INDEX); - expect(jobInfo.metadata).to.equal("some metadata"); - expect(jobInfo.owner).to.equal(await user.getAddress()); - expect(jobInfo.provider).to.equal(await provider.getAddress()); - expect(jobInfo.rate).to.equal(JOB_RATE_1); - expect(jobInfo.balance).to.equal(TOTAL_DEPOSIT_AMOUNT.sub(NOTICE_PERIOD_COST)); - expect(jobInfo.lastSettled).to.equal(currentTimestamp + NOTICE_PERIOD); - expect(creditBalance).to.equal(ADDITIONAL_CREDIT_DEPOSIT_AMOUNT); - - // User Balance - const userBalanceExpected = SIGNER1_INITIAL_FUND.sub(INITIAL_DEPOSIT_AMOUNT).sub(ADDITIONAL_USDC_DEPOSIT_AMOUNT).sub(ADDITIONAL_CREDIT_DEPOSIT_AMOUNT); - expect(await token.balanceOf(addrs[1])).to.be.within(userBalanceExpected.sub(JOB_RATE_1), userBalanceExpected.add(JOB_RATE_1)); - - // MarketV1 Balance - const marketv1BalanceExpected = TOTAL_DEPOSIT_AMOUNT.sub(NOTICE_PERIOD_COST); - expect(await token.balanceOf(marketv1.address)).to.be.within(marketv1BalanceExpected.sub(JOB_RATE_1), marketv1BalanceExpected.add(JOB_RATE_1)); - }); - - it("should revert when depositing to job without enough USDC approved", async () => { - await marketv1 - .connect(user) - .jobOpen("some metadata", await provider.getAddress(), JOB_RATE_1, INITIAL_DEPOSIT_AMOUNT); - - // Deposit 30 Credit and 1000 USDC - await creditToken.connect(user).approve(marketv1.address, ADDITIONAL_CREDIT_DEPOSIT_AMOUNT); - await expect(marketv1 - .connect(user) - .jobDeposit(INITIAL_JOB_INDEX, ADDITIONAL_CREDIT_DEPOSIT_AMOUNT.add(SIGNER1_INITIAL_FUND))).to.be.revertedWith("ERC20: insufficient allowance"); - }); - - it("should revert when user does not have enough USDC balance", async () => { - await marketv1 - .connect(user) - .jobOpen("some metadata", await provider.getAddress(), JOB_RATE_1, INITIAL_DEPOSIT_AMOUNT); - - // Deposit 30 Credit and 1000 USDC - await creditToken.connect(user).approve(marketv1.address, ADDITIONAL_CREDIT_DEPOSIT_AMOUNT); - await token.connect(user).approve(marketv1.address, SIGNER1_INITIAL_FUND); - await expect(marketv1 - .connect(user) - .jobDeposit(INITIAL_JOB_INDEX, ADDITIONAL_CREDIT_DEPOSIT_AMOUNT.add(SIGNER1_INITIAL_FUND))).to.be.revertedWith("ERC20: transfer amount exceeds balance"); - }); - - it("should revert when balance is below notice period cost", async () => { - // Open Job - await token.connect(user).approve(marketv1.address, SIGNER1_INITIAL_FUND.add(usdc(1000))); - await marketv1 - .connect(user) - .jobOpen("some metadata", await provider.getAddress(), JOB_RATE_1, SIGNER1_INITIAL_FUND); - - // 100_000 seconds passed (spend 1000 usdc) - await time.increaseTo(INITIAL_TIMESTAMP + 100_000); - - // Deposit 25 USDC - await expect(marketv1 - .connect(user) - .jobDeposit(INITIAL_JOB_INDEX, usdc(25))).to.be.revertedWith("insufficient funds to deposit"); - }); - }); - }); - - describe("Job Withdraw", function () { - const TWO_MINUTES = 60 * 2; - const SEVEN_MINUTES = 60 * 7; - - takeSnapshotBeforeAndAfterEveryTest(async () => { }); - - describe("USDC Only", function () { - const INITIAL_DEPOSIT_AMOUNT = usdc(50); - const NOTICE_PERIOD_COST = calcNoticePeriodCost(JOB_RATE_1); - const TOTAL_WITHDRAW_AMOUNT = usdc(10); - - beforeEach(async () => { - // Deposit 50 USDC - await token.connect(user).approve(marketv1.address, INITIAL_DEPOSIT_AMOUNT); - await marketv1 - .connect(user) - .jobOpen("some metadata", await provider.getAddress(), JOB_RATE_1, INITIAL_DEPOSIT_AMOUNT); - }); - - it("should withdraw from job immediately", async () => { - const withdrawAmount = usdc(10); - - // Job Withdraw - await marketv1 - .connect(user) - .jobWithdraw(INITIAL_JOB_INDEX, withdrawAmount); - - const currentTimestamp = (await ethers.provider.getBlock('latest')).timestamp; - - let jobInfo = await marketv1.jobs(INITIAL_JOB_INDEX); - expect(jobInfo.metadata).to.equal("some metadata"); - expect(jobInfo.owner).to.equal(await user.getAddress()); - expect(jobInfo.provider).to.equal(await provider.getAddress()); - expect(jobInfo.rate).to.equal(JOB_RATE_1); - expect(jobInfo.balance).to.equal(INITIAL_DEPOSIT_AMOUNT.sub(NOTICE_PERIOD_COST).sub(withdrawAmount)); - expect(jobInfo.lastSettled).to.equal(currentTimestamp + NOTICE_PERIOD); - - expect(await token.balanceOf(await user.getAddress())).to.equal(SIGNER1_INITIAL_FUND.sub(INITIAL_DEPOSIT_AMOUNT).add(withdrawAmount)); - expect(await token.balanceOf(await provider.getAddress())).to.equal(NOTICE_PERIOD_COST); - expect(await token.balanceOf(marketv1.address)).to.equal(INITIAL_DEPOSIT_AMOUNT.sub(NOTICE_PERIOD_COST).sub(withdrawAmount)); - }); - - it("should withdraw from job before lastSettled", async () => { - - // 2 minutes passed - await time.increaseTo(INITIAL_TIMESTAMP + TWO_MINUTES); - - const providerBalanceBefore = await token.balanceOf(await provider.getAddress()); - - // Job Withdraw - await marketv1 - .connect(user) - .jobWithdraw(INITIAL_JOB_INDEX, TOTAL_WITHDRAW_AMOUNT); // withdraw 10 USDC - - const SETTLED_AMOUNT = calcAmountToPay(JOB_RATE_1, TWO_MINUTES); - - // Job info after Withdrawal - let jobInfo = await marketv1.jobs(INITIAL_JOB_INDEX); - expect(jobInfo.metadata).to.equal("some metadata"); - expect(jobInfo.owner).to.equal(await user.getAddress()); - expect(jobInfo.provider).to.equal(await provider.getAddress()); - expect(jobInfo.rate).to.equal(JOB_RATE_1); - const jobBalanceExpected = INITIAL_DEPOSIT_AMOUNT.sub(NOTICE_PERIOD_COST).sub(SETTLED_AMOUNT).sub(TOTAL_WITHDRAW_AMOUNT); - expect(jobInfo.balance).to.be.within(jobBalanceExpected.sub(JOB_RATE_1), jobBalanceExpected.add(JOB_RATE_1)); - expect(jobInfo.lastSettled).to.be.within(INITIAL_TIMESTAMP + FIVE_MINUTES + TWO_MINUTES - 3, INITIAL_TIMESTAMP + FIVE_MINUTES + TWO_MINUTES + 3); - - // Check User USDC balance - const userUSDCBalanceExpected = SIGNER1_INITIAL_FUND.sub(INITIAL_DEPOSIT_AMOUNT).add(TOTAL_WITHDRAW_AMOUNT); - expect(await token.balanceOf(await user.getAddress())).to.be.within(userUSDCBalanceExpected.sub(JOB_RATE_1), userUSDCBalanceExpected.add(JOB_RATE_1)); - - // Check Provider USDC balance - const providerUSDCBalanceExpected = NOTICE_PERIOD_COST.add(SETTLED_AMOUNT); - expect(await token.balanceOf(await provider.getAddress())).to.be.within(providerUSDCBalanceExpected.sub(JOB_RATE_1), providerUSDCBalanceExpected.add(JOB_RATE_1)); - - // Check MarketV1 USDC balance - const marketv1BalanceExpected = INITIAL_DEPOSIT_AMOUNT.sub(NOTICE_PERIOD_COST).sub(SETTLED_AMOUNT); - expect(await token.balanceOf(marketv1.address)).to.be.within(marketv1BalanceExpected.sub(JOB_RATE_1), marketv1BalanceExpected.add(JOB_RATE_1)); - }); - - it("should withdraw from job after lastSettled with settlement", async () => { - const settledAmountExpected = calcAmountToPay(JOB_RATE_1, TWO_MINUTES); - - // 7 minutes passed after Job Open - await time.increaseTo(INITIAL_TIMESTAMP + NOTICE_PERIOD + TWO_MINUTES); - await marketv1 - .connect(user) - .jobWithdraw(INITIAL_JOB_INDEX, TOTAL_WITHDRAW_AMOUNT); - - expect(await token.balanceOf(await user.getAddress())).to.equal(SIGNER1_INITIAL_FUND.sub(INITIAL_DEPOSIT_AMOUNT).add(TOTAL_WITHDRAW_AMOUNT)); - - const providerBalanceExpected = calcNoticePeriodCost(JOB_RATE_1).add(settledAmountExpected); - expect(await token.balanceOf(await provider.getAddress())).to.be.within(providerBalanceExpected.sub(JOB_RATE_1), providerBalanceExpected.add(JOB_RATE_1)); - - const marketv1BalanceExpected = INITIAL_DEPOSIT_AMOUNT.sub(NOTICE_PERIOD_COST).sub(settledAmountExpected); - expect(await token.balanceOf(marketv1.address)).to.be.within(marketv1BalanceExpected.sub(JOB_RATE_1), marketv1BalanceExpected.add(JOB_RATE_1)); - }); - - it("should revert when withdrawing from non existent job", async () => { - const max_uint256_bytes32 = ethers.utils.hexZeroPad(ethers.constants.MaxUint256.toHexString(), 32); - - await expect(marketv1 - .connect(user) - .jobWithdraw(max_uint256_bytes32, usdc(100))).to.be.revertedWith("only job owner"); - }); - - it("should revert when withdrawing from third party job", async () => { - await expect(marketv1 - .connect(signers[3]) // neither owner nor provider - .jobWithdraw(INITIAL_JOB_INDEX, usdc(100))).to.be.revertedWith("only job owner"); - }); - - it("should revert when balance is below notice period cost", async () => { - // deposited 50 USDC - // 0.01 USDC/s - // notice period cost: 0.01 * 300 = 3 USDC - // 47 USDC left - - await time.increaseTo(INITIAL_TIMESTAMP + NOTICE_PERIOD + 4500); // spend 45 USDC (300 + 4500 seconds passed) - - await expect(marketv1 - .connect(user) - .jobWithdraw(INITIAL_JOB_INDEX, usdc(1))).to.be.revertedWith("insufficient funds to withdraw"); - }); - - it("should revert when withdrawal request amount exceeds max withdrawable amount", async () => { - // Current balance: 47 USDC - - await expect(marketv1 - .connect(user) - .jobWithdraw(INITIAL_JOB_INDEX, usdc(48))).to.be.revertedWith("withdrawal amount exceeds job balance"); - }); - }); - - describe("Credit Only", function () { - const INITIAL_DEPOSIT_AMOUNT = usdc(50); - const NOTICE_PERIOD_COST = calcNoticePeriodCost(JOB_RATE_1); - const CREDIT_WITHDRAW_AMOUNT = usdc(10); - const TOTAL_WITHDRAW_AMOUNT = usdc(10); - - beforeEach(async () => { - // Deposit 50 Credit - await creditToken.connect(user).approve(marketv1.address, INITIAL_DEPOSIT_AMOUNT); - await marketv1.connect(user).jobOpen("some metadata", await provider.getAddress(), JOB_RATE_1, INITIAL_DEPOSIT_AMOUNT); - }); - - it("should withdraw from job immediately", async () => { - // Job Withdraw - await marketv1 - .connect(user) - .jobWithdraw(INITIAL_JOB_INDEX, CREDIT_WITHDRAW_AMOUNT); // withdraw 10 Credit - - const jobInfo = await marketv1.jobs(INITIAL_JOB_INDEX); - const jobCreditBalance = await marketv1.jobCreditBalance(INITIAL_JOB_INDEX); - const currentTimestamp = (await ethers.provider.getBlock('latest')).timestamp; - - expect(jobInfo.metadata).to.equal("some metadata"); - expect(jobInfo.owner).to.equal(await user.getAddress()); - expect(jobInfo.provider).to.equal(await provider.getAddress()); - expect(jobInfo.rate).to.equal(JOB_RATE_1); - expect(jobInfo.balance).to.equal(INITIAL_DEPOSIT_AMOUNT.sub(NOTICE_PERIOD_COST).sub(CREDIT_WITHDRAW_AMOUNT)); - expect(jobInfo.lastSettled).to.equal(currentTimestamp + NOTICE_PERIOD); - - expect(jobCreditBalance).to.equal(INITIAL_DEPOSIT_AMOUNT.sub(NOTICE_PERIOD_COST).sub(CREDIT_WITHDRAW_AMOUNT)); - - // User Balance - const userCreditBalanceExpected = SIGNER1_INITIAL_FUND.sub(INITIAL_DEPOSIT_AMOUNT).add(CREDIT_WITHDRAW_AMOUNT); - expect(await creditToken.balanceOf(await user.getAddress())).to.equal(userCreditBalanceExpected); - - // Provider Balance - const providerCreditBalanceExpected = 0; - const providerUSDCBalanceExpected = NOTICE_PERIOD_COST; - expect(await creditToken.balanceOf(await provider.getAddress())).to.equal(providerCreditBalanceExpected); - expect(await token.balanceOf(await provider.getAddress())).to.equal(providerUSDCBalanceExpected); - - const marketv1CreditBalanceExpected = INITIAL_DEPOSIT_AMOUNT.sub(NOTICE_PERIOD_COST).sub(CREDIT_WITHDRAW_AMOUNT); - expect(await creditToken.balanceOf(marketv1.address)).to.equal(marketv1CreditBalanceExpected); - }); - - it("should withdraw from job before lastSettled", async () => { - // 2 minutes passed - await time.increaseTo(INITIAL_TIMESTAMP + TWO_MINUTES); - const SETTLED_AMOUNT = calcAmountToPay(JOB_RATE_1, TWO_MINUTES); - - // Job Withdraw - await marketv1 - .connect(user) - .jobWithdraw(INITIAL_JOB_INDEX, CREDIT_WITHDRAW_AMOUNT); // withdraw 10 Credit - - // Job info after Withdrawal - let jobInfo = await marketv1.jobs(INITIAL_JOB_INDEX); - const jobCreditBalance = await marketv1.jobCreditBalance(INITIAL_JOB_INDEX); - expect(jobInfo.metadata).to.equal("some metadata"); - expect(jobInfo.owner).to.equal(await user.getAddress()); - expect(jobInfo.provider).to.equal(await provider.getAddress()); - expect(jobInfo.rate).to.equal(JOB_RATE_1); - const jobBalanceExpected = INITIAL_DEPOSIT_AMOUNT.sub(NOTICE_PERIOD_COST).sub(SETTLED_AMOUNT).sub(TOTAL_WITHDRAW_AMOUNT); - expect(jobInfo.balance).to.be.within(jobBalanceExpected.sub(JOB_RATE_1), jobBalanceExpected.add(JOB_RATE_1)); - expect(jobInfo.lastSettled).to.be.within(INITIAL_TIMESTAMP + FIVE_MINUTES + TWO_MINUTES - 3, INITIAL_TIMESTAMP + FIVE_MINUTES + TWO_MINUTES + 3); - - const jobCreditBalanceExpected = INITIAL_DEPOSIT_AMOUNT.sub(NOTICE_PERIOD_COST).sub(SETTLED_AMOUNT).sub(TOTAL_WITHDRAW_AMOUNT); - expect(jobCreditBalance).to.be.within(jobCreditBalanceExpected.sub(JOB_RATE_1), jobCreditBalanceExpected.add(JOB_RATE_1)); - - // User Balance - const userCreditBalanceExpected = SIGNER1_INITIAL_FUND.sub(INITIAL_DEPOSIT_AMOUNT).add(TOTAL_WITHDRAW_AMOUNT); - const userUSDCBalanceExpected = 0; - expect(await creditToken.balanceOf(await user.getAddress())).to.equal(userCreditBalanceExpected); - expect(await token.balanceOf(await user.getAddress())).to.equal(SIGNER1_INITIAL_FUND); - - // Provider Balance - const providerCreditBalanceExpected = 0; - const providerUSDCBalanceExpected = NOTICE_PERIOD_COST.add(SETTLED_AMOUNT); - expect(await creditToken.balanceOf(await provider.getAddress())).to.equal(providerCreditBalanceExpected); - expect(await token.balanceOf(await provider.getAddress())).to.be.within(providerUSDCBalanceExpected.sub(JOB_RATE_1), providerUSDCBalanceExpected.add(JOB_RATE_1)); - - // Check MarketV1 USDC balance - const marketv1CreditBalanceExpected = INITIAL_DEPOSIT_AMOUNT.sub(NOTICE_PERIOD_COST).sub(SETTLED_AMOUNT).sub(TOTAL_WITHDRAW_AMOUNT); - const marketv1USDCBalanceExpected = 0; - expect(await creditToken.balanceOf(marketv1.address)).to.be.within(marketv1CreditBalanceExpected.sub(JOB_RATE_1), marketv1CreditBalanceExpected.add(JOB_RATE_1)); - expect(await token.balanceOf(marketv1.address)).to.equal(marketv1USDCBalanceExpected); - }); - - it("should withdraw from job after lastSettled with settlement", async () => { - const settledAmountExpected = calcAmountToPay(JOB_RATE_1, SEVEN_MINUTES); - - // 7 minutes passed after Job Open - await time.increaseTo(INITIAL_TIMESTAMP + SEVEN_MINUTES); - await marketv1 - .connect(user) - .jobWithdraw(INITIAL_JOB_INDEX, TOTAL_WITHDRAW_AMOUNT); - - - // User Balance - const userCreditBalanceExpected = SIGNER1_INITIAL_FUND.sub(INITIAL_DEPOSIT_AMOUNT).add(TOTAL_WITHDRAW_AMOUNT); - const userUSDCBalanceExpected = SIGNER1_INITIAL_FUND; - expect(await creditToken.balanceOf(await user.getAddress())).to.equal(userCreditBalanceExpected); - expect(await token.balanceOf(await user.getAddress())).to.equal(userUSDCBalanceExpected); - - // Provider Balance - const providerCreditBalanceExpected = 0; - const providerUSDCBalanceExpected = NOTICE_PERIOD_COST.add(settledAmountExpected); - expect(await creditToken.balanceOf(await provider.getAddress())).to.equal(providerCreditBalanceExpected); - expect(await token.balanceOf(await provider.getAddress())).to.be.within(providerUSDCBalanceExpected.sub(JOB_RATE_1), providerUSDCBalanceExpected.add(JOB_RATE_1)); - - // Check MarketV1 Balance - const marketv1CreditBalanceExpected = INITIAL_DEPOSIT_AMOUNT.sub(NOTICE_PERIOD_COST).sub(settledAmountExpected).sub(TOTAL_WITHDRAW_AMOUNT); - const marketv1USDCBalanceExpected = 0; - expect(await creditToken.balanceOf(marketv1.address)).to.be.within(marketv1CreditBalanceExpected.sub(JOB_RATE_1), marketv1CreditBalanceExpected.add(JOB_RATE_1)); - expect(await token.balanceOf(marketv1.address)).to.equal(marketv1USDCBalanceExpected); - }); - }); - - describe("Both Credit and USDC", function () { - const INITIAL_DEPOSIT_AMOUNT = usdc(50); - const INITIAL_CREDIT_DEPOSIT_AMOUNT = usdc(40); - const NOTICE_PERIOD_COST = calcNoticePeriodCost(JOB_RATE_1); - const TOTAL_WITHDRAW_AMOUNT = usdc(20); // 13 USDC + 7 Credit - - beforeEach(async () => { - // Deposit 10 Credit, 40 USDC - await creditToken.connect(user).approve(marketv1.address, INITIAL_CREDIT_DEPOSIT_AMOUNT); - await marketv1.connect(user).jobOpen("some metadata", await provider.getAddress(), JOB_RATE_1, INITIAL_DEPOSIT_AMOUNT); - }); - - it("should withdraw only USDC", async () => { - const userCreditBalanceBefore = await creditToken.balanceOf(await user.getAddress()); - const userUSDCBalanceBefore = await token.balanceOf(await user.getAddress()); - - const USDC_WITHDRAWAL_AMOUNT = usdc(5); - - // Job Withdraw - await marketv1 - .connect(user) - .jobWithdraw(INITIAL_JOB_INDEX, USDC_WITHDRAWAL_AMOUNT); // withdraw 5 usdc - - // Job Info - const jobInfo = await marketv1.jobs(INITIAL_JOB_INDEX); - const jobCreditBalance = await marketv1.jobCreditBalance(INITIAL_JOB_INDEX); - const currentTimestamp = (await ethers.provider.getBlock('latest')).timestamp; - - expect(jobInfo.metadata).to.equal("some metadata"); - expect(jobInfo.owner).to.equal(await user.getAddress()); - expect(jobInfo.provider).to.equal(await provider.getAddress()); - expect(jobInfo.rate).to.equal(JOB_RATE_1); - const jobBalanceExpected = INITIAL_DEPOSIT_AMOUNT.sub(NOTICE_PERIOD_COST).sub(TOTAL_WITHDRAW_AMOUNT); - expect(jobInfo.balance).to.be.within(jobBalanceExpected.sub(JOB_RATE_1), jobBalanceExpected.add(JOB_RATE_1)); - expect(jobInfo.lastSettled).to.equal(currentTimestamp + NOTICE_PERIOD); - const jobCreditBalanceExpected = INITIAL_CREDIT_DEPOSIT_AMOUNT.sub(NOTICE_PERIOD_COST); - expect(jobCreditBalance).to.equal(jobCreditBalanceExpected); - - // User Balance - const userCreditBalanceExpected = userCreditBalanceBefore; - const userUSDCBalanceExpected = userUSDCBalanceBefore.sub(USDC_WITHDRAWAL_AMOUNT); - expect(await creditToken.balanceOf(await user.getAddress())).to.equal(userCreditBalanceExpected); - expect(await token.balanceOf(await user.getAddress())).to.be.within(userUSDCBalanceExpected.sub(JOB_RATE_1), userUSDCBalanceExpected.add(JOB_RATE_1)); - - // Provider Balance - const providerCreditBalanceExpected = 0; - const providerUSDCBalanceExpected = NOTICE_PERIOD_COST; - expect(await creditToken.balanceOf(await provider.getAddress())).to.equal(providerCreditBalanceExpected); - expect(await token.balanceOf(await provider.getAddress())).to.equal(providerUSDCBalanceExpected); - - const marketv1CreditBalanceExpected = INITIAL_CREDIT_DEPOSIT_AMOUNT.sub(NOTICE_PERIOD_COST); - const marketv1USDCBalanceExpected = INITIAL_DEPOSIT_AMOUNT.sub(INITIAL_CREDIT_DEPOSIT_AMOUNT).sub(USDC_WITHDRAWAL_AMOUNT); - expect(await creditToken.balanceOf(marketv1.address)).to.be.within(marketv1CreditBalanceExpected.sub(JOB_RATE_1), marketv1CreditBalanceExpected.add(JOB_RATE_1)); - expect(await token.balanceOf(marketv1.address)).to.be.within(marketv1USDCBalanceExpected.sub(JOB_RATE_1), marketv1USDCBalanceExpected.add(JOB_RATE_1)); - }); - - it("should withdraw both USDC and Credit", async () => { - const userCreditBalanceBefore = await creditToken.balanceOf(await user.getAddress()); - const userUSDCBalanceBefore = await token.balanceOf(await user.getAddress()); - - const withdrawnUSDCAmountExpected = ((await marketv1.jobs(INITIAL_JOB_INDEX)).balance).sub(await marketv1.jobCreditBalance(INITIAL_JOB_INDEX)); - const withdrawnCreditAmountExpected = TOTAL_WITHDRAW_AMOUNT.sub(withdrawnUSDCAmountExpected); - // Job Withdraw - await marketv1 - .connect(user) - .jobWithdraw(INITIAL_JOB_INDEX, TOTAL_WITHDRAW_AMOUNT); // withdraw 20 (13 USDC + 7 Credit) - - const currentTimestamp = (await ethers.provider.getBlock('latest')).timestamp; - - // Job Info - const jobInfo = await marketv1.jobs(INITIAL_JOB_INDEX); - const jobCreditBalance = await marketv1.jobCreditBalance(INITIAL_JOB_INDEX); - expect(jobInfo.metadata).to.equal("some metadata"); - expect(jobInfo.owner).to.equal(await user.getAddress()); - expect(jobInfo.provider).to.equal(await provider.getAddress()); - expect(jobInfo.rate).to.equal(JOB_RATE_1); - const jobBalanceExpected = INITIAL_DEPOSIT_AMOUNT.sub(NOTICE_PERIOD_COST).sub(TOTAL_WITHDRAW_AMOUNT); - expect(jobInfo.balance).to.be.within(jobBalanceExpected.sub(JOB_RATE_1), jobBalanceExpected.add(JOB_RATE_1)); - expect(jobInfo.lastSettled).to.equal(currentTimestamp + NOTICE_PERIOD); - expect(jobCreditBalance).to.equal(INITIAL_CREDIT_DEPOSIT_AMOUNT.sub(NOTICE_PERIOD_COST).sub(withdrawnCreditAmountExpected)); - - // User Balance - const userCreditBalanceExpected = userCreditBalanceBefore.add(withdrawnCreditAmountExpected); - const userUSDCBalanceExpected = userUSDCBalanceBefore.add(withdrawnUSDCAmountExpected); - expect(await creditToken.balanceOf(await user.getAddress())).to.equal(userCreditBalanceExpected); - expect(await token.balanceOf(await user.getAddress())).to.equal(userUSDCBalanceExpected); - - // Provider Balance - const providerCreditBalanceExpected = 0; - const providerUSDCBalanceExpected = NOTICE_PERIOD_COST; - expect(await creditToken.balanceOf(await provider.getAddress())).to.equal(providerCreditBalanceExpected); - expect(await token.balanceOf(await provider.getAddress())).to.equal(providerUSDCBalanceExpected); - - // MarketV1 Balance - const marketv1CreditBalanceExpected = INITIAL_CREDIT_DEPOSIT_AMOUNT.sub(NOTICE_PERIOD_COST).sub(withdrawnCreditAmountExpected); - expect(await creditToken.balanceOf(marketv1.address)).to.equal(marketv1CreditBalanceExpected); - expect(await token.balanceOf(marketv1.address)).to.equal(0); - }); - }); - }); - - describe("Job Revise Rate", function () { - const JOB_LOWER_RATE = BN.from(5).e16().div(10); - const JOB_HIGHER_RATE = BN.from(2).e16(); - - const initialDeposit = usdc(50); - const initialBalance = initialDeposit.sub(calcNoticePeriodCost(JOB_RATE_1)); - - takeSnapshotBeforeAndAfterEveryTest(async () => { }); - - beforeEach(async () => { - await token.connect(user).approve(marketv1.address, initialDeposit); - await marketv1 - .connect(user) - .jobOpen("some metadata", await provider.getAddress(), JOB_RATE_1, initialDeposit); - }); - - it("should revise rate higher", async () => { - const currentTimestamp = (await ethers.provider.getBlock('latest')).timestamp; - - await marketv1 - .connect(user) - .jobReviseRate(INITIAL_JOB_INDEX, JOB_HIGHER_RATE); - - const jobInfo = await marketv1.jobs(INITIAL_JOB_INDEX); - expect(jobInfo.rate).to.equal(JOB_HIGHER_RATE); - expect(jobInfo.balance).to.equal(initialBalance); - - expect(jobInfo.lastSettled).to.equal(currentTimestamp + FIVE_MINUTES); - expect(await token.balanceOf(await user.getAddress())).to.equal(SIGNER1_INITIAL_FUND.sub(initialDeposit)); - expect(await token.balanceOf(await provider.getAddress())).to.equal(calcNoticePeriodCost(JOB_RATE_1)); - expect(await token.balanceOf(marketv1.address)).to.equal(initialBalance); - }); - - it("should revise rate lower", async () => { - const currentTimestamp = (await ethers.provider.getBlock('latest')).timestamp; - - await marketv1 - .connect(user) - .jobReviseRate(INITIAL_JOB_INDEX, JOB_LOWER_RATE); - - const jobInfo = await marketv1.jobs(INITIAL_JOB_INDEX); - expect(jobInfo.rate).to.equal(JOB_LOWER_RATE); - expect(jobInfo.balance).to.equal(initialBalance); - expect(jobInfo.lastSettled).to.equal(currentTimestamp + FIVE_MINUTES); - expect(await token.balanceOf(await user.getAddress())).to.equal(SIGNER1_INITIAL_FUND.sub(initialDeposit)); - expect(await token.balanceOf(await provider.getAddress())).to.equal(calcNoticePeriodCost(JOB_RATE_1)); - expect(await token.balanceOf(marketv1.address)).to.equal(initialBalance); - }); - - it("should revert when initiating rate revision for non existent job", async () => { - await expect(marketv1 - .connect(user) - .jobReviseRate(ethers.utils.hexZeroPad("0x01", 32), JOB_HIGHER_RATE)).to.be.revertedWith("only job owner"); - }); - - it("should revert when initiating rate revision for third party job", async () => { - await expect(marketv1 - .connect(signers[3]) // neither owner nor provider - .jobReviseRate(INITIAL_JOB_INDEX, JOB_HIGHER_RATE)).to.be.revertedWith("only job owner"); - }); - - const HIGHER_RATE = BN.from(2).e16(); // 0.02 USDC/s - const LOWER_RATE = BN.from(5).e15(); // 0.005 USDC/s - - describe("CASE 1: Revising Rate immediately after job open", function () { - - describe("when rate is higher", function () { - - it("should spend notice period cost only", async () => { - const noticePeriodCostExpected = calcNoticePeriodCost(JOB_RATE_1); - - await marketv1 - .connect(user) - .jobReviseRate(INITIAL_JOB_INDEX, HIGHER_RATE); - - // Job info after Rate Revision - const jobInfo = await marketv1.jobs(INITIAL_JOB_INDEX); - expect(jobInfo.rate).to.equal(HIGHER_RATE); - const jobBalanceExpected = initialBalance.sub(noticePeriodCostExpected); - expect(jobInfo.balance).to.be.within(jobBalanceExpected.sub(JOB_RATE_1), jobBalanceExpected.add(JOB_RATE_1)); - - const userBalanceExpected = SIGNER1_INITIAL_FUND.sub(initialDeposit).sub(noticePeriodCostExpected); - expect(await token.balanceOf(await user.getAddress())).to.be.within(userBalanceExpected.sub(JOB_RATE_1), userBalanceExpected.add(JOB_RATE_1)); - - const providerBalanceExpected = noticePeriodCostExpected; - expect(await token.balanceOf(await provider.getAddress())).to.be.within(providerBalanceExpected.sub(JOB_RATE_1), providerBalanceExpected.add(JOB_RATE_1)); - - const marketv1BalanceExpected = jobBalanceExpected; - expect(await token.balanceOf(marketv1.address)).to.be.within(marketv1BalanceExpected.sub(JOB_RATE_1), marketv1BalanceExpected.add(JOB_RATE_1)); - }); - }); - - describe("when rate is lower", function () { - it("should spend notice period cost only", async () => { - const noticePeriodCostExpected = calcNoticePeriodCost(JOB_RATE_1); - - await marketv1 - .connect(user) - .jobReviseRate(INITIAL_JOB_INDEX, LOWER_RATE); - // Job info after Rate Revision - const jobInfo = await marketv1.jobs(INITIAL_JOB_INDEX); - expect(jobInfo.rate).to.equal(LOWER_RATE); - const jobBalanceExpected = initialDeposit.sub(noticePeriodCostExpected); - expect(jobInfo.balance).to.be.within(jobBalanceExpected.sub(JOB_RATE_1), jobBalanceExpected.add(JOB_RATE_1)); - - const userBalanceExpected = SIGNER1_INITIAL_FUND.sub(noticePeriodCostExpected); - expect(await token.balanceOf(await user.getAddress())).to.be.within(userBalanceExpected.sub(JOB_RATE_1), userBalanceExpected.add(JOB_RATE_1)); - - const providerBalanceExpected = noticePeriodCostExpected; - expect(await token.balanceOf(await provider.getAddress())).to.be.within(providerBalanceExpected.sub(JOB_RATE_1), providerBalanceExpected.add(JOB_RATE_1)); - - const marketv1BalanceExpected = jobBalanceExpected; - expect(await token.balanceOf(marketv1.address)).to.be.within(marketv1BalanceExpected.sub(JOB_RATE_1), marketv1BalanceExpected.add(JOB_RATE_1)); - }); - }); - }); - - describe("CASE 2: Revising Rate 2 minutes after job open", function () { - const TWO_MINUTES = 60 * 2; - const SEVEN_MINUTES = 60 * 7; - - describe("when rate is higher", function () { - it("should spend notice period cost + 3 minutes worth tokens with higher rate", async () => { - // 5 min * initial rate + 3 min * higher rate - const usdcSpentExpected = calcNoticePeriodCost(JOB_RATE_1).add(calcAmountToPay(HIGHER_RATE, TWO_MINUTES)); - const initialTimestamp = (await ethers.provider.getBlock('latest')).timestamp; - - await time.increaseTo(INITIAL_TIMESTAMP + TWO_MINUTES); - - await marketv1 - .connect(user) - .jobReviseRate(INITIAL_JOB_INDEX, HIGHER_RATE); - - const jobInfo = await marketv1.jobs(INITIAL_JOB_INDEX); - expect(jobInfo.rate).to.equal(HIGHER_RATE); - - const jobBalanceExpected = initialDeposit.sub(usdcSpentExpected); - expect(jobInfo.balance).to.be.within(jobBalanceExpected.sub(JOB_RATE_1), jobBalanceExpected.add(JOB_RATE_1)); - - const lastSettledTimestampExpected = (await ethers.provider.getBlock('latest')).timestamp + FIVE_MINUTES; - expect(jobInfo.lastSettled).to.be.within(lastSettledTimestampExpected - 3, lastSettledTimestampExpected + 3); - - const userBalanceExpected = SIGNER1_INITIAL_FUND.sub(initialDeposit).sub(usdcSpentExpected); - expect(await token.balanceOf(await user.getAddress())).to.be.within(userBalanceExpected.sub(JOB_RATE_1), userBalanceExpected.add(JOB_RATE_1)); - - const providerBalanceExpected = usdcSpentExpected; - expect(await token.balanceOf(await provider.getAddress())).to.be.within(providerBalanceExpected.sub(JOB_RATE_1), providerBalanceExpected.add(JOB_RATE_1)); - - const marketv1BalanceExpected = jobBalanceExpected; - expect(await token.balanceOf(marketv1.address)).to.be.within(marketv1BalanceExpected.sub(JOB_RATE_1), marketv1BalanceExpected.add(JOB_RATE_1)); - }); - }); - - describe("when rate is lower", function () { - it("should spend notice period cost + 3 minutes worth tokens with initial rate", async () => { - // 5 min * initial rate + 3 min * initial rate - const usdcSpentExpected = calcNoticePeriodCost(JOB_RATE_1).add(calcAmountToPay(JOB_RATE_1, TWO_MINUTES)); - const initialTimestamp = (await ethers.provider.getBlock('latest')).timestamp; - - await time.increaseTo(INITIAL_TIMESTAMP + TWO_MINUTES); - - await marketv1 - .connect(user) - .jobReviseRate(INITIAL_JOB_INDEX, LOWER_RATE); + const amountPaidThisSettle = calcAmountToPay(JOB_RATE_1, DURATION_TO_SETTLE); + const jobBalanceExpected = initialBalance.sub(amountPaidThisSettle); + expect(jobInfo.balance).to.be.closeTo(jobBalanceExpected, 2); - const jobInfo = await marketv1.jobs(INITIAL_JOB_INDEX); - expect(jobInfo.rate).to.equal(LOWER_RATE); - - const jobBalanceExpected = initialDeposit.sub(usdcSpentExpected); - expect(jobInfo.balance).to.be.within(jobBalanceExpected.sub(JOB_RATE_1), jobBalanceExpected.add(JOB_RATE_1)); - - const lastSettledTimestampExpected = (await ethers.provider.getBlock('latest')).timestamp + FIVE_MINUTES; - expect(jobInfo.lastSettled).to.be.within(lastSettledTimestampExpected - 3, lastSettledTimestampExpected + 3); - - const userBalanceExpected = SIGNER1_INITIAL_FUND.sub(initialDeposit).sub(usdcSpentExpected); - expect(await token.balanceOf(await user.getAddress())).to.be.within(userBalanceExpected.sub(JOB_RATE_1), userBalanceExpected.add(JOB_RATE_1)); - - const providerBalanceExpected = usdcSpentExpected; - expect(await token.balanceOf(await provider.getAddress())).to.be.within(providerBalanceExpected.sub(JOB_RATE_1), providerBalanceExpected.add(JOB_RATE_1)); - - const marketv1BalanceExpected = jobBalanceExpected; - expect(await token.balanceOf(marketv1.address)).to.be.within(marketv1BalanceExpected.sub(JOB_RATE_1), marketv1BalanceExpected.add(JOB_RATE_1)); - }); - }); - }); - - describe("CASE 3: Revising Rate exactly after notice period", function () { - const TEN_MINUTES = 60 * 10; - - describe("when rate is higher", function () { - it("should spend 5 minutes worth tokens with initial rate and 5 minutes worth tokens with higher rate", async () => { - const initialTimestamp = (await ethers.provider.getBlock('latest')).timestamp; - const firstNoticePeriodCost = calcNoticePeriodCost(JOB_RATE_1); - const secondNoticePeriodCost = calcNoticePeriodCost(HIGHER_RATE); - - await time.increaseTo(INITIAL_TIMESTAMP + NOTICE_PERIOD); - - await marketv1 - .connect(user) - .jobReviseRate(INITIAL_JOB_INDEX, HIGHER_RATE); - - const jobInfo = await marketv1.jobs(INITIAL_JOB_INDEX); - expect(jobInfo.rate).to.equal(HIGHER_RATE); - - const jobBalanceExpected = initialDeposit.sub(firstNoticePeriodCost).sub(secondNoticePeriodCost); - expect(jobInfo.balance).to.be.within(jobBalanceExpected.sub(JOB_RATE_1), jobBalanceExpected.add(JOB_RATE_1)); - - const lastSettledTimestampExpected = (await ethers.provider.getBlock('latest')).timestamp + FIVE_MINUTES; - expect(jobInfo.lastSettled).to.be.within(lastSettledTimestampExpected - 3, lastSettledTimestampExpected + 3); - - const userBalanceExpected = SIGNER1_INITIAL_FUND.sub(initialDeposit); - expect(await token.balanceOf(await user.getAddress())).to.be.within(userBalanceExpected.sub(JOB_RATE_1), userBalanceExpected.add(JOB_RATE_1)); - - const providerBalanceExpected = firstNoticePeriodCost.add(secondNoticePeriodCost); - expect(await token.balanceOf(await provider.getAddress())).to.be.within(providerBalanceExpected.sub(JOB_RATE_1), providerBalanceExpected.add(JOB_RATE_1)); - - const marketv1BalanceExpected = jobBalanceExpected; - expect(await token.balanceOf(marketv1.address)).to.be.within(marketv1BalanceExpected.sub(JOB_RATE_1), marketv1BalanceExpected.add(JOB_RATE_1)); - }); - }); - - describe("when rate is lower", function () { - it("should spend 5 minutes worth tokens with initial rate and 5 minutes worth tokens with initial rate", async () => { - const initialTimestamp = (await ethers.provider.getBlock('latest')).timestamp; - const firstNoticePeriodCost = calcNoticePeriodCost(JOB_RATE_1); - const secondNoticePeriodCost = calcNoticePeriodCost(LOWER_RATE); - - await time.increaseTo(INITIAL_TIMESTAMP + NOTICE_PERIOD); - - await marketv1 - .connect(user) - .jobReviseRate(INITIAL_JOB_INDEX, LOWER_RATE); - - const jobInfo = await marketv1.jobs(INITIAL_JOB_INDEX); - expect(jobInfo.rate).to.equal(LOWER_RATE); - - const jobBalanceExpected = initialDeposit.sub(firstNoticePeriodCost).sub(secondNoticePeriodCost); - expect(jobInfo.balance).to.be.within(jobBalanceExpected.sub(JOB_RATE_1), jobBalanceExpected.add(JOB_RATE_1)); - - const lastSettledTimestampExpected = (await ethers.provider.getBlock('latest')).timestamp + FIVE_MINUTES; - expect(jobInfo.lastSettled).to.be.within(lastSettledTimestampExpected - 3, lastSettledTimestampExpected + 3); - - const userBalanceExpected = SIGNER1_INITIAL_FUND.sub(initialDeposit); - expect(await token.balanceOf(await user.getAddress())).to.be.within(userBalanceExpected.sub(JOB_RATE_1), userBalanceExpected.add(JOB_RATE_1)); - - const providerBalanceExpected = firstNoticePeriodCost.add(secondNoticePeriodCost); - expect(await token.balanceOf(await provider.getAddress())).to.be.within(providerBalanceExpected.sub(JOB_RATE_1), providerBalanceExpected.add(JOB_RATE_1)); - - const marketv1BalanceExpected = jobBalanceExpected; - expect(await token.balanceOf(marketv1.address)).to.be.within(marketv1BalanceExpected.sub(JOB_RATE_1), marketv1BalanceExpected.add(JOB_RATE_1)); - }); - }); - }); - - describe("CASE 4: Revising Rate 2 minutes after notice period", function () { - const TWO_MINUTES = 60 * 2; - const TWELVE_MINUTES = 60 * 12; - - describe("when rate is higher", function () { - it("should spend 7 minutes worth tokens with initial rate and 5 minutes worth tokens with higher rate", async () => { - const initialTimestamp = (await ethers.provider.getBlock('latest')).timestamp; - const firstNoticePeriodCost = calcNoticePeriodCost(JOB_RATE_1); - const secondNoticePeriodCost = calcNoticePeriodCost(HIGHER_RATE); - - await time.increaseTo(INITIAL_TIMESTAMP + NOTICE_PERIOD + TWO_MINUTES); - - await marketv1 - .connect(user) - .jobReviseRate(INITIAL_JOB_INDEX, HIGHER_RATE); - - const jobInfo = await marketv1.jobs(INITIAL_JOB_INDEX); - expect(jobInfo.rate).to.equal(HIGHER_RATE); - - const jobBalanceExpected = initialDeposit.sub(firstNoticePeriodCost).sub(secondNoticePeriodCost); - expect(jobInfo.balance).to.be.within(jobBalanceExpected.sub(JOB_RATE_1), jobBalanceExpected.add(JOB_RATE_1)); - - const lastSettledTimestampExpected = (await ethers.provider.getBlock('latest')).timestamp + FIVE_MINUTES; - expect(jobInfo.lastSettled).to.be.within(lastSettledTimestampExpected - 3, lastSettledTimestampExpected + 3); + expect(jobInfo.lastSettled).to.be.closeTo(TIME_JOB_SETTLE_TARGET, 3); - const userBalanceExpected = SIGNER1_INITIAL_FUND.sub(initialDeposit); - expect(await token.balanceOf(await user.getAddress())).to.be.within(userBalanceExpected.sub(JOB_RATE_1), userBalanceExpected.add(JOB_RATE_1)); - - const providerBalanceExpected = firstNoticePeriodCost.add(secondNoticePeriodCost); - expect(await token.balanceOf(await provider.getAddress())).to.be.within(providerBalanceExpected.sub(JOB_RATE_1), providerBalanceExpected.add(JOB_RATE_1)); - - const marketv1BalanceExpected = jobBalanceExpected; - expect(await token.balanceOf(marketv1.address)).to.be.within(marketv1BalanceExpected.sub(JOB_RATE_1), marketv1BalanceExpected.add(JOB_RATE_1)); - }); - }); - - describe("when rate is lower", function () { - it("should spend 7 minutes worth tokens with initial rate and 5 minutes worth tokens with initial rate", async () => { - const initialTimestamp = (await ethers.provider.getBlock('latest')).timestamp; - const firstNoticePeriodCost = calcNoticePeriodCost(JOB_RATE_1); - const secondNoticePeriodCost = calcNoticePeriodCost(JOB_RATE_1); - - await time.increaseTo(INITIAL_TIMESTAMP + NOTICE_PERIOD + TWO_MINUTES); - - await marketv1 - .connect(user) - .jobReviseRate(INITIAL_JOB_INDEX, LOWER_RATE); - - const jobInfo = await marketv1.jobs(INITIAL_JOB_INDEX); - expect(jobInfo.rate).to.equal(LOWER_RATE); - - const jobBalanceExpected = initialDeposit.sub(firstNoticePeriodCost).sub(secondNoticePeriodCost); - expect(jobInfo.balance).to.be.within(jobBalanceExpected.sub(JOB_RATE_1), jobBalanceExpected.add(JOB_RATE_1)); - - const lastSettledTimestampExpected = (await ethers.provider.getBlock('latest')).timestamp + FIVE_MINUTES; - expect(jobInfo.lastSettled).to.be.within(lastSettledTimestampExpected - 3, lastSettledTimestampExpected + 3); - - const userBalanceExpected = SIGNER1_INITIAL_FUND.sub(initialDeposit); - expect(await token.balanceOf(await user.getAddress())).to.be.within(userBalanceExpected.sub(JOB_RATE_1), userBalanceExpected.add(JOB_RATE_1)); - - const providerBalanceExpected = firstNoticePeriodCost.add(secondNoticePeriodCost); - expect(await token.balanceOf(await provider.getAddress())).to.be.within(providerBalanceExpected.sub(JOB_RATE_1), providerBalanceExpected.add(JOB_RATE_1)); - - const marketv1BalanceExpected = jobBalanceExpected; - expect(await token.balanceOf(marketv1.address)).to.be.within(marketv1BalanceExpected.sub(JOB_RATE_1), marketv1BalanceExpected.add(JOB_RATE_1)); - }); - }); - }); - }); - - describe("Job Close", function () { - const initialDeposit = usdc(50); - const initialBalance = initialDeposit.sub(calcNoticePeriodCost(JOB_RATE_1)); - - takeSnapshotBeforeAndAfterEveryTest(async () => { }); - - describe("USDC Only", function () { - beforeEach(async () => { - await token.connect(user).approve(marketv1.address, initialDeposit); - await marketv1 - .connect(user) - .jobOpen("some metadata", await provider.getAddress(), JOB_RATE_1, initialDeposit); - }); - - it("should close job", async () => { - // Job Close - await marketv1 - .connect(user) - .jobClose(INITIAL_JOB_INDEX); // here, user should get back (initial deposit - notice period cost) - - const jobInfo = await marketv1.jobs(INITIAL_JOB_INDEX); - expect(jobInfo.metadata).to.equal(""); - expect(jobInfo.owner).to.equal(ethers.constants.AddressZero); - expect(jobInfo.provider).to.equal(ethers.constants.AddressZero); - expect(jobInfo.rate).to.equal(0); - expect(jobInfo.balance).to.equal(0); - expect(jobInfo.lastSettled).to.equal(0); - - const userBalanceExpected = SIGNER1_INITIAL_FUND.sub(initialDeposit).sub(calcNoticePeriodCost(JOB_RATE_1)); - expect(await token.balanceOf(await user.getAddress())).to.be.within(userBalanceExpected.sub(JOB_RATE_1), userBalanceExpected.add(JOB_RATE_1)); - }); - - it("should revert when closing non existent job", async () => { - await expect(marketv1 - .connect(user) - .jobClose(ethers.utils.hexZeroPad("0x01", 32))).to.be.revertedWith("only job owner"); - }); - - it("should revert when closing third party job", async () => { - await expect(marketv1 - .connect(signers[3]) // neither owner nor provider - .jobClose(INITIAL_JOB_INDEX)).to.be.revertedWith("only job owner"); - }); - - describe("Scenario 1: Closing Job immediately after opening", function () { - it("should spend notice period cost only", async () => { - await marketv1 - .connect(user) - .jobClose(INITIAL_JOB_INDEX); - - const noticePeriodCostExpected = calcNoticePeriodCost(JOB_RATE_1); - - // user balance after = initial fund - notice period cost - expect(await token.balanceOf(await user.getAddress())).to.equal(SIGNER1_INITIAL_FUND.sub(noticePeriodCostExpected)); - // provider balance after = notice period cost - expect(await token.balanceOf(await provider.getAddress())).to.equal(noticePeriodCostExpected); - // marketv1 balance after = 0 - expect(await token.balanceOf(marketv1.address)).to.equal(0); - }); - }); - - describe("Scenario 2: Closing Job 2 minutes after opening (before notice period)", function () { - it("should spend notice period cost only", async () => { - const TWO_MINUTES = 60 * 2; - - await time.increaseTo(INITIAL_TIMESTAMP + TWO_MINUTES); - - await marketv1 - .connect(user) - .jobClose(INITIAL_JOB_INDEX); - - const noticePeriodCostExpected = calcNoticePeriodCost(JOB_RATE_1); - - // user balance after = initial fund - 3 minutes worth tokens - notice period cost - const userBalanceExpected = SIGNER1_INITIAL_FUND.sub(usdc(TWO_MINUTES)).sub(noticePeriodCostExpected); - expect(await token.balanceOf(await user.getAddress())).to.be.within(userBalanceExpected.sub(JOB_RATE_1), userBalanceExpected.add(JOB_RATE_1)); - - // provider balance after = 2 minutes worth tokens + notice period cost - const providerBalanceExpected = usdc(TWO_MINUTES).add(noticePeriodCostExpected); - expect(await token.balanceOf(await provider.getAddress())).to.be.within(providerBalanceExpected.sub(JOB_RATE_1), providerBalanceExpected.add(JOB_RATE_1)); - - // marketv1 balance after = 0 - expect(await token.balanceOf(marketv1.address)).to.equal(0); - }); - }); - - describe("Scenario 3: Closing Job exactly after notice period", function () { - it("should spend 10 minutes worth tokens", async () => { - const usdcSpentExpected = calcAmountToPay(JOB_RATE_1, FIVE_MINUTES).add(calcNoticePeriodCost(JOB_RATE_1)); - - await time.increaseTo(INITIAL_TIMESTAMP + NOTICE_PERIOD); - - await marketv1 - .connect(user) - .jobClose(INITIAL_JOB_INDEX); - - // user balance after = initial fund - 5 minutes worth tokens - notice period cost - const userBalanceExpected = SIGNER1_INITIAL_FUND.sub(usdcSpentExpected); - expect(await token.balanceOf(await user.getAddress())).to.be.within(userBalanceExpected.sub(JOB_RATE_1), userBalanceExpected.add(JOB_RATE_1)); + expect(finalJobCreditBalance).to.equal(0); - // provider balance after = 5 minutes worth tokens + notice period cost - const providerBalanceExpected = usdcSpentExpected; - expect(await token.balanceOf(await provider.getAddress())).to.be.within(providerBalanceExpected.sub(JOB_RATE_1), providerBalanceExpected.add(JOB_RATE_1)); + const providerPaymentThisSettle = amountPaidThisSettle; + const providerBalanceExpected = noticeCostPaidAtOpen.add(providerPaymentThisSettle); + expect(await token.balanceOf(await provider.getAddress())).to.be.closeTo(providerBalanceExpected, 2); - expect(await token.balanceOf(marketv1.address)).to.equal(0); + expect(await token.balanceOf(marketv1.address)).to.be.closeTo(jobBalanceExpected, 2); + expect(await creditToken.balanceOf(marketv1.address)).to.equal(0); }); }); - - describe("Scenario 4: Closing Job 2 minutes after notice period", function () { - it("should spend 12 minutes worth tokens", async () => { - const SEVEN_MINUTES = 60 * 7; - const usdcSpentExpected = calcAmountToPay(JOB_RATE_1, SEVEN_MINUTES).add(calcNoticePeriodCost(JOB_RATE_1)); - - await time.increaseTo(INITIAL_TIMESTAMP + NOTICE_PERIOD + SEVEN_MINUTES); - - await marketv1 - .connect(user) - .jobClose(INITIAL_JOB_INDEX); - - const userBalanceExpected = SIGNER1_INITIAL_FUND.sub(usdcSpentExpected); - expect(await token.balanceOf(await user.getAddress())).to.be.within(userBalanceExpected.sub(JOB_RATE_1), userBalanceExpected.add(JOB_RATE_1)); - - const providerBalanceExpected = usdcSpentExpected; - expect(await token.balanceOf(await provider.getAddress())).to.be.within(providerBalanceExpected.sub(JOB_RATE_1), providerBalanceExpected.add(JOB_RATE_1)); - - expect(await token.balanceOf(marketv1.address)).to.equal(0); - }); - }); - }); - - describe("Credit Only", function () { - const INITIAL_DEPOSIT_AMOUNT = usdc(50); - const NOTICE_PERIOD_COST = calcNoticePeriodCost(JOB_RATE_1); - - beforeEach(async () => { - // Deposit 50 Credit - await creditToken.connect(user).approve(marketv1.address, INITIAL_DEPOSIT_AMOUNT); - await marketv1.connect(user).jobOpen("some metadata", await provider.getAddress(), JOB_RATE_1, INITIAL_DEPOSIT_AMOUNT); - }); - - it("should close job and withdraw all credit", async () => { - expect(await marketv1.jobCreditBalance(INITIAL_JOB_INDEX)).to.equal(INITIAL_DEPOSIT_AMOUNT.sub(NOTICE_PERIOD_COST)); - const userCreditBalanceBefore = await creditToken.balanceOf(await user.getAddress()); - const userUSDCBalanceBefore = await token.balanceOf(await user.getAddress()); - - // Close job - await marketv1.connect(user).jobClose(INITIAL_JOB_INDEX); - - const userCreditBalanceExpected = userCreditBalanceBefore.add(INITIAL_DEPOSIT_AMOUNT.sub(NOTICE_PERIOD_COST)); - const userUSDCBalanceExpected = userUSDCBalanceBefore; - expect(await creditToken.balanceOf(await user.getAddress())).to.equal(userCreditBalanceExpected); - expect(await token.balanceOf(await user.getAddress())).to.equal(userUSDCBalanceExpected); - }); - }); - - describe("Both Credit and USDC", function () { - const INITIAL_DEPOSIT_AMOUNT = usdc(50); - const INITIAL_CREDIT_DEPOSIT_AMOUNT = usdc(40); - const NOTICE_PERIOD_COST = calcNoticePeriodCost(JOB_RATE_1); - - beforeEach(async () => { - // Deposit 40 Credit, 10 USDC - await creditToken.connect(user).approve(marketv1.address, INITIAL_CREDIT_DEPOSIT_AMOUNT); - await marketv1.connect(user).jobOpen("some metadata", await provider.getAddress(), JOB_RATE_1, INITIAL_DEPOSIT_AMOUNT); - }); - - it("should close job and withdraw all Credit and USDC", async () => { - const userCreditBalanceBefore = await creditToken.balanceOf(await user.getAddress()); - const userUSDCBalanceBefore = await token.balanceOf(await user.getAddress()); - - // Close job - await marketv1.connect(user).jobClose(INITIAL_JOB_INDEX); - - const userCreditBalanceExpected = userCreditBalanceBefore.add(INITIAL_CREDIT_DEPOSIT_AMOUNT.sub(NOTICE_PERIOD_COST)); - const userUSDCBalanceExpected = userUSDCBalanceBefore.add(INITIAL_DEPOSIT_AMOUNT.sub(INITIAL_CREDIT_DEPOSIT_AMOUNT)); - expect(await creditToken.balanceOf(await user.getAddress())).to.be.within(userCreditBalanceExpected.sub(JOB_RATE_1), userCreditBalanceExpected.add(JOB_RATE_1)); - expect(await token.balanceOf(await user.getAddress())).to.be.within(userUSDCBalanceExpected.sub(JOB_RATE_1), userUSDCBalanceExpected.add(JOB_RATE_1)); - }); - }); - }); - - describe("Metdata Update", function () { - const initialDeposit = usdc(50); - - takeSnapshotBeforeAndAfterEveryTest(async () => { }); - - beforeEach(async () => { - await token.connect(user).approve(marketv1.address, initialDeposit); - await marketv1 - .connect(user) - .jobOpen("some metadata", await provider.getAddress(), JOB_RATE_1, initialDeposit); - }); - - it("should update metadata", async () => { - await marketv1 - .connect(user) - .jobMetadataUpdate(INITIAL_JOB_INDEX, "some updated metadata"); - - const jobInfo2 = await marketv1.jobs(INITIAL_JOB_INDEX); - expect(jobInfo2.metadata).to.equal("some updated metadata"); - }); - - it("should revert when updating metadata of other jobs", async () => { - await expect(marketv1 - .connect(signers[3]) // neither owner nor provider - .jobMetadataUpdate(INITIAL_JOB_INDEX, "some updated metadata")).to.be.revertedWith("only job owner"); - }); - }); - - describe("Emergency Withdraw", function () { - const NUM_TOTAL_JOB = 5; - const INITIAL_DEPOSIT_AMOUNT = usdc(50); - let TOTAL_DEPOSIT_AMOUNT = BN.from(0); - let jobs: string[] = []; - let deposits: BN[] = []; - - beforeEach(async () => { - await marketv1.connect(admin).grantRole(await marketv1.EMERGENCY_WITHDRAW_ROLE(), await admin2.getAddress()); - - // open 5 jobs - for (let i = 0; i < NUM_TOTAL_JOB; i++) { - const EXTRA_DEPOSIT_AMOUNT = usdc(i * 10); - const DEPOSIT_AMOUNT = INITIAL_DEPOSIT_AMOUNT.add(EXTRA_DEPOSIT_AMOUNT); - - // list of jobs and deposits - jobs.push(await marketv1.jobIndex()); - deposits.push(DEPOSIT_AMOUNT); - // total credit deposit amount - TOTAL_DEPOSIT_AMOUNT = TOTAL_DEPOSIT_AMOUNT.add(DEPOSIT_AMOUNT); - - // open job only with credit - await creditToken.connect(user).approve(marketv1.address, DEPOSIT_AMOUNT); - await marketv1.connect(user).jobOpen("some metadata", await provider.getAddress(), JOB_RATE_1, DEPOSIT_AMOUNT); - } }); - - it("should revert when withdrawing to address without EMERGENCY_WITHDRAW_ROLE", async () => { - await expect(marketv1 - .connect(admin) - .emergencyWithdrawCredit(await user.getAddress(), [INITIAL_JOB_INDEX])).to.be.revertedWith("only to emergency withdraw role"); - }); - - it("should revert when non-admin calls emergencyWithdrawCredit", async () => { - await expect(marketv1 - .connect(user) - .emergencyWithdrawCredit(await user.getAddress(), jobs)).to.be.revertedWith("only admin"); - }); - - it("should settle all jobs and withdraw all credit", async () => { - const totalSettledAmountExpected = calcNoticePeriodCost(JOB_RATE_1).mul(NUM_TOTAL_JOB); - - await marketv1.connect(admin).emergencyWithdrawCredit(await admin2.getAddress(), jobs); - - const CURRENT_TIMESTAMP = (await ethers.provider.getBlock('latest')).timestamp; - for (let i = 0; i < NUM_TOTAL_JOB; i++) { - // fetch job info - const jobInfo = await marketv1.jobs(jobs[i]); - - // should settle all jobs - expect(jobInfo.lastSettled).to.equal((CURRENT_TIMESTAMP + NOTICE_PERIOD).toString()); - - // job credit balance should be 0 - expect(await marketv1.jobCreditBalance(jobs[i])).to.equal(0); - } - - // withdrawal recipient - const withdrawalAmountExpected = TOTAL_DEPOSIT_AMOUNT.sub(totalSettledAmountExpected); - expect(await creditToken.balanceOf(await admin2.getAddress())).to.be.within(withdrawalAmountExpected.sub(JOB_RATE_1), withdrawalAmountExpected.add(JOB_RATE_1)); - - // Provider - expect(await token.balanceOf(await provider.getAddress())).to.be.within(totalSettledAmountExpected.sub(JOB_RATE_1), totalSettledAmountExpected.add(JOB_RATE_1)); - - // MarketV1 - expect(await creditToken.balanceOf(marketv1.address)).to.equal(0); - }); - }) + }); }); \ No newline at end of file From 5be7248af36784f52a52d237a87ebf1f68f25291 Mon Sep 17 00:00:00 2001 From: Prateek Date: Thu, 22 May 2025 12:38:40 +0400 Subject: [PATCH 3/3] fix: tests --- test/enclaves/MarketV1.ts | 1647 ++++++++++++++++++++++++++++++++----- 1 file changed, 1454 insertions(+), 193 deletions(-) diff --git a/test/enclaves/MarketV1.ts b/test/enclaves/MarketV1.ts index 50dfd51..951e410 100644 --- a/test/enclaves/MarketV1.ts +++ b/test/enclaves/MarketV1.ts @@ -66,7 +66,8 @@ const calcNoticePeriodCost = (rate: BN) => { }; const calcAmountToPay = (rate: BN, duration: number) => { - return rate.mul(BN.from(duration)).add(10 ** 12 - 1).div(10 ** 12); + const DECIMALS = BN.from("1000000000000"); + return rate.mul(BN.from(duration)).add(DECIMALS.sub(1)).div(DECIMALS); } const incrementJobId = (jobId: string, increment: number) => { @@ -312,6 +313,8 @@ describe("MarketV1", function () { let admin2: Signer; let INITIAL_JOB_INDEX: string; + let JOB_OPENED_TIMESTAMP: number; + before(async function () { signers = await ethers.getSigners(); @@ -463,17 +466,21 @@ describe("MarketV1", function () { const initialBalance = usdc(50); const noticePeriodCost = calcNoticePeriodCost(JOB_RATE_1); - await marketv1 + const tx = await marketv1 .connect(user) .jobOpen("some metadata", await provider.getAddress(), JOB_RATE_1, initialBalance); + const receipt = await tx.wait(); + const block = await ethers.provider.getBlock(receipt.blockHash); + const jobOpenTs = block.timestamp; + const jobInfo = await marketv1.jobs(INITIAL_JOB_INDEX); expect(jobInfo.metadata).to.equal("some metadata"); expect(jobInfo.owner).to.equal(await user.getAddress()); expect(jobInfo.provider).to.equal(await provider.getAddress()); expect(jobInfo.rate).to.equal(JOB_RATE_1); expect(jobInfo.balance).to.equal(initialBalance.sub(noticePeriodCost)); - expect(jobInfo.lastSettled).to.be.within(INITIAL_TIMESTAMP, INITIAL_TIMESTAMP + 1); + expect(jobInfo.lastSettled).to.equal(jobOpenTs); expect(jobInfo.maxRate).to.equal(JOB_RATE_1); expect(await token.balanceOf(await user.getAddress())).to.equal(SIGNER1_INITIAL_FUND.sub(initialBalance)); @@ -517,17 +524,20 @@ describe("MarketV1", function () { const noticePeriodCost = calcNoticePeriodCost(JOB_RATE_1); await creditToken.connect(user).approve(marketv1.address, initialBalance); - await marketv1 + const tx = await marketv1 .connect(user) .jobOpen("some metadata", await provider.getAddress(), JOB_RATE_1, initialBalance); - + const receipt = await tx.wait(); + const block = await ethers.provider.getBlock(receipt.blockHash); + const jobOpenTs = block.timestamp; + const jobInfo = await marketv1.jobs(INITIAL_JOB_INDEX); expect(jobInfo.metadata).to.equal("some metadata"); expect(jobInfo.owner).to.equal(await user.getAddress()); expect(jobInfo.provider).to.equal(await provider.getAddress()); expect(jobInfo.rate).to.equal(JOB_RATE_1); expect(jobInfo.balance).to.equal(initialBalance.sub(noticePeriodCost)); - expect(jobInfo.lastSettled).to.be.within(INITIAL_TIMESTAMP, INITIAL_TIMESTAMP + 1); + expect(jobInfo.lastSettled).to.equal(jobOpenTs); expect(await marketv1.jobCreditBalance(INITIAL_JOB_INDEX)).to.equal(initialBalance.sub(noticePeriodCost)); }); @@ -568,7 +578,6 @@ describe("MarketV1", function () { describe("Job Settle", function () { const initialDeposit = usdc(50); const initialBalance = initialDeposit.sub(calcNoticePeriodCost(JOB_RATE_1)); - let jobOpenActualTimestamp: number; takeSnapshotBeforeAndAfterEveryTest(async () => { }); @@ -576,58 +585,47 @@ describe("MarketV1", function () { beforeEach(async () => { await token.connect(user).approve(marketv1.address, initialDeposit); - await marketv1 + const tx = await marketv1 .connect(user) .jobOpen("some metadata", await provider.getAddress(), JOB_RATE_1, initialDeposit); - jobOpenActualTimestamp = (await ethers.provider.getBlock('latest')).timestamp; + const receipt = await tx.wait(); + const block = await ethers.provider.getBlock(receipt.blockHash); + JOB_OPENED_TIMESTAMP = block.timestamp; }); describe("CASE1: Settle Job immediately after Job Open", function () { - it("should have balance and lastSettled reflecting initial state after open", async () => { - const jobBeforeSettle = await marketv1.jobs(INITIAL_JOB_INDEX); - await marketv1.connect(user).jobSettle(INITIAL_JOB_INDEX); - const jobInfo = await marketv1.jobs(INITIAL_JOB_INDEX); + it("should lose notice period cost", async () => { + const tx = await marketv1.connect(user).jobSettle(INITIAL_JOB_INDEX); + const receipt = await tx.wait(); + const block = await ethers.provider.getBlock(receipt.blockHash); + const jobSettleTs = block.timestamp; - expect(jobInfo.balance).to.equal(initialBalance); - expect(jobInfo.lastSettled).to.be.closeTo(jobOpenActualTimestamp, 2); - }); - }); - - describe("CASE2: Settle Job 2 minutes after Job Open", function () { - it("should settle for 2 minutes", async () => { - const DURATION_TO_SETTLE = TWO_MINUTES; - const expectedSettledTimestamp = jobOpenActualTimestamp + DURATION_TO_SETTLE; - await time.increaseTo(expectedSettledTimestamp); - - await marketv1.connect(user).jobSettle(INITIAL_JOB_INDEX); const jobInfo = await marketv1.jobs(INITIAL_JOB_INDEX); - - expect(jobInfo.lastSettled).to.be.closeTo(expectedSettledTimestamp, 2); - expect(jobInfo.balance).to.equal(initialBalance.sub(calcAmountToPay(JOB_RATE_1, DURATION_TO_SETTLE))); + expect(jobInfo.balance).to.equal(initialBalance.sub(calcAmountToPay(JOB_RATE_1, jobInfo.lastSettled.sub(JOB_OPENED_TIMESTAMP).toNumber()))); + expect(jobInfo.lastSettled).to.equal(jobSettleTs); }); }); - - describe("CASE3: Settle Job 1 second before notice period", function () { - it("should settle for (notice period - 1 second)", async () => { - const DURATION_TO_SETTLE = NOTICE_PERIOD - 1; - const expectedSettledTimestamp = jobOpenActualTimestamp + DURATION_TO_SETTLE; - await time.increaseTo(expectedSettledTimestamp); + describe("CASE2: Settle Job 2 minutes after Job Open", function () { + it("Balance should decrease by 2 minutes + Notice period worth", async () => { + const TWO_MINUTES = 60 * 2; + await time.increaseTo(JOB_OPENED_TIMESTAMP + TWO_MINUTES); + await marketv1.connect(user).jobSettle(INITIAL_JOB_INDEX); const jobInfo = await marketv1.jobs(INITIAL_JOB_INDEX); - - expect(jobInfo.lastSettled).to.be.closeTo(expectedSettledTimestamp, 2); - expect(jobInfo.balance).to.equal(initialBalance.sub(calcAmountToPay(JOB_RATE_1, DURATION_TO_SETTLE))); + expect(jobInfo.lastSettled).to.be.equal(JOB_OPENED_TIMESTAMP + TWO_MINUTES); + expect(jobInfo.balance).to.equal( + initialBalance.sub(calcAmountToPay(JOB_RATE_1, jobInfo.lastSettled.sub(JOB_OPENED_TIMESTAMP).toNumber())), + ); }); }); describe("CASE4: Settle Job 2 minutes after notice period", function () { - it("should spend for (notice period + 2 minutes)", async () => { - const DURATION_TO_SETTLE = NOTICE_PERIOD + TWO_MINUTES; - const expectedSettledTimestamp = jobOpenActualTimestamp + DURATION_TO_SETTLE; - await time.increaseTo(expectedSettledTimestamp); - - const noticeCostPaidAtOpen = calcNoticePeriodCost(JOB_RATE_1); + it("should spend notice period cost and 2 minutes + notice period worth tokens", async () => { + const TWO_MINUTES = 60 * 2; + await time.increaseTo(JOB_OPENED_TIMESTAMP + NOTICE_PERIOD + TWO_MINUTES); + + // Job Settle await marketv1.connect(user).jobSettle(INITIAL_JOB_INDEX); const jobInfo = await marketv1.jobs(INITIAL_JOB_INDEX); @@ -636,112 +634,128 @@ describe("MarketV1", function () { expect(jobInfo.provider).to.equal(await provider.getAddress()); expect(jobInfo.rate).to.equal(JOB_RATE_1); - const amountPaidThisSettle = calcAmountToPay(JOB_RATE_1, DURATION_TO_SETTLE); - const jobBalanceExpected = initialBalance.sub(amountPaidThisSettle); - expect(jobInfo.balance).to.be.closeTo(jobBalanceExpected, 2); + const jobBalanceExpected = initialBalance.sub(calcAmountToPay(JOB_RATE_1, TWO_MINUTES)); + expect(jobInfo.balance).to.be.within(jobBalanceExpected.sub(JOB_RATE_1), jobBalanceExpected.add(JOB_RATE_1)); - expect(jobInfo.lastSettled).to.be.closeTo(expectedSettledTimestamp, 2); + const lastSettledTimestampExpected = JOB_OPENED_TIMESTAMP + NOTICE_PERIOD + TWO_MINUTES; + expect(jobInfo.lastSettled).to.equal(lastSettledTimestampExpected); + // User balance const userBalanceExpected = SIGNER1_INITIAL_FUND.sub(initialDeposit); expect(await token.balanceOf(await user.getAddress())).to.equal(userBalanceExpected); - const providerBalanceExpected = noticeCostPaidAtOpen.add(amountPaidThisSettle); - expect(await token.balanceOf(await provider.getAddress())).to.be.closeTo(providerBalanceExpected, 2); + // Provider balance + const providerBalanceExpected = calcAmountToPay(JOB_RATE_1, TWO_MINUTES).add(calcNoticePeriodCost(JOB_RATE_1)); + expect(await token.balanceOf(await provider.getAddress())).to.be.within(providerBalanceExpected.sub(JOB_RATE_1), providerBalanceExpected.add(JOB_RATE_1)); - expect(await token.balanceOf(marketv1.address)).to.be.closeTo(jobBalanceExpected, 2); + // MarketV1 balance + const marketv1BalanceExpected = initialBalance.sub(calcAmountToPay(JOB_RATE_1, TWO_MINUTES)); + expect(await token.balanceOf(marketv1.address)).to.be.within(marketv1BalanceExpected.sub(JOB_RATE_1), marketv1BalanceExpected.add(JOB_RATE_1)); }); }); }); describe("Credit Only", function () { beforeEach(async () => { + // await token.connect(user).approve(marketv1.address, initialDeposit); await creditToken.connect(user).approve(marketv1.address, initialDeposit); - await marketv1 + const tx = await marketv1 .connect(user) .jobOpen("some metadata", await provider.getAddress(), JOB_RATE_1, initialDeposit); - jobOpenActualTimestamp = (await ethers.provider.getBlock('latest')).timestamp; + const receipt = await tx.wait(); + const block = await ethers.provider.getBlock(receipt.blockHash); + JOB_OPENED_TIMESTAMP = block.timestamp; }); describe("CASE1: Settle Job immediately after Job Open", function () { - it("should have balance and lastSettled reflecting initial state after open", async () => { - const jobBeforeSettle = await marketv1.jobs(INITIAL_JOB_INDEX); - const creditBalanceBeforeSettle = await marketv1.jobCreditBalance(INITIAL_JOB_INDEX); + it("should deduct notice period worth from credit balance", async () => { + const tx = await marketv1.connect(user).jobSettle(INITIAL_JOB_INDEX); + const receipt = await tx.wait(); + const block = await ethers.provider.getBlock(receipt.blockHash); + const jobSettleTs = block.timestamp; - await marketv1.connect(user).jobSettle(INITIAL_JOB_INDEX); const jobInfo = await marketv1.jobs(INITIAL_JOB_INDEX); - const creditBalanceAfterSettle = await marketv1.jobCreditBalance(INITIAL_JOB_INDEX); - + expect(jobInfo.lastSettled).to.equal(jobSettleTs); expect(jobInfo.balance).to.equal(initialBalance); - expect(jobInfo.lastSettled).to.be.closeTo(jobOpenActualTimestamp, 2); - expect(creditBalanceAfterSettle).to.equal(creditBalanceBeforeSettle); - expect(creditBalanceAfterSettle).to.equal(initialBalance); + expect(await marketv1.jobCreditBalance(INITIAL_JOB_INDEX)).to.equal( + initialDeposit.sub(calcNoticePeriodCost(JOB_RATE_1)), + ); }); }); describe("CASE2: Settle Job 2 minutes after Job Open", function () { - it("should settle for 2 minutes using credit", async () => { - const DURATION_TO_SETTLE = TWO_MINUTES; - const expectedSettledTimestamp = jobOpenActualTimestamp + DURATION_TO_SETTLE; - await time.increaseTo(expectedSettledTimestamp); - - const amountToSettle = calcAmountToPay(JOB_RATE_1, DURATION_TO_SETTLE); + it("should revert before lastSettled", async () => { + const TWO_MINUTES = 60 * 2; + await time.increaseTo(INITIAL_TIMESTAMP + TWO_MINUTES); - await marketv1.connect(user).jobSettle(INITIAL_JOB_INDEX); + const tx = await marketv1.connect(user).jobSettle(INITIAL_JOB_INDEX); + const receipt = await tx.wait(); + const block = await ethers.provider.getBlock(receipt.blockHash); + const jobSettleTs = block.timestamp; const jobInfo = await marketv1.jobs(INITIAL_JOB_INDEX); - const jobCreditBal = await marketv1.jobCreditBalance(INITIAL_JOB_INDEX); - - expect(jobInfo.lastSettled).to.be.closeTo(expectedSettledTimestamp, 2); - expect(jobInfo.balance).to.equal(initialBalance.sub(amountToSettle)); - expect(jobCreditBal).to.equal(initialBalance.sub(amountToSettle)); + expect(jobInfo.lastSettled).to.be.equal(jobSettleTs); + expect(jobInfo.lastSettled).to.be.closeTo(INITIAL_TIMESTAMP + TWO_MINUTES, 1); + expect(jobInfo.balance).to.equal(initialBalance.sub(calcAmountToPay(JOB_RATE_1, jobInfo.lastSettled.sub(JOB_OPENED_TIMESTAMP).toNumber()))); + expect(await marketv1.jobCreditBalance(INITIAL_JOB_INDEX)).to.equal( + initialDeposit.sub(calcNoticePeriodCost(JOB_RATE_1)).sub(calcAmountToPay(JOB_RATE_1, jobInfo.lastSettled.sub(JOB_OPENED_TIMESTAMP).toNumber()) + )); }); }); describe("CASE3: Settle Job exactly after notice period", function () { - it("should settle for notice period duration using credit", async () => { - const DURATION_TO_SETTLE = NOTICE_PERIOD; - const expectedSettledTimestamp = jobOpenActualTimestamp + DURATION_TO_SETTLE; - await time.increaseTo(expectedSettledTimestamp); - - const amountToSettle = calcAmountToPay(JOB_RATE_1, DURATION_TO_SETTLE); + it("should revert before lastSettled", async () => { + await time.increaseTo(INITIAL_TIMESTAMP + NOTICE_PERIOD); - await marketv1.connect(user).jobSettle(INITIAL_JOB_INDEX); + const tx = await marketv1.connect(user).jobSettle(INITIAL_JOB_INDEX); + const receipt = await tx.wait(); + const block = await ethers.provider.getBlock(receipt.blockHash); + const jobSettleTs = block.timestamp; const jobInfo = await marketv1.jobs(INITIAL_JOB_INDEX); - const jobCreditBal = await marketv1.jobCreditBalance(INITIAL_JOB_INDEX); - - expect(jobInfo.lastSettled).to.be.closeTo(expectedSettledTimestamp, 2); - expect(jobInfo.balance).to.equal(initialBalance.sub(amountToSettle)); - expect(jobCreditBal).to.equal(initialBalance.sub(amountToSettle)); + expect(jobInfo.lastSettled).to.be.equal(jobSettleTs); + expect(jobInfo.lastSettled).to.be.closeTo(INITIAL_TIMESTAMP + NOTICE_PERIOD, 1); + expect(jobInfo.balance).to.equal(initialBalance.sub(calcAmountToPay(JOB_RATE_1, jobInfo.lastSettled.sub(JOB_OPENED_TIMESTAMP).toNumber()))); + expect(await marketv1.jobCreditBalance(INITIAL_JOB_INDEX)).to.equal( + initialDeposit.sub(calcNoticePeriodCost(JOB_RATE_1)).sub(calcAmountToPay(JOB_RATE_1, jobInfo.lastSettled.sub(JOB_OPENED_TIMESTAMP).toNumber()) + )); }); }); describe("CASE4: Settle Job 2 minutes after notice period", function () { - it("should spend notice period cost and 2 minutes worth tokens from credit", async () => { - const DURATION_TO_SETTLE = NOTICE_PERIOD + TWO_MINUTES; - const expectedSettledTimestamp = jobOpenActualTimestamp + DURATION_TO_SETTLE; - await time.increaseTo(expectedSettledTimestamp); + it("should spend notice period cost and 2 minutes worth tokens", async () => { + const TWO_MINUTES = 60 * 2; + await time.increaseTo(INITIAL_TIMESTAMP + NOTICE_PERIOD + TWO_MINUTES); - const noticeCostPaidAtOpen = calcNoticePeriodCost(JOB_RATE_1); + // Job Settle await marketv1.connect(user).jobSettle(INITIAL_JOB_INDEX); + // Job Info After Settle const jobInfo = await marketv1.jobs(INITIAL_JOB_INDEX); - const jobCreditBal = await marketv1.jobCreditBalance(INITIAL_JOB_INDEX); expect(jobInfo.metadata).to.equal("some metadata"); expect(jobInfo.owner).to.equal(await user.getAddress()); expect(jobInfo.provider).to.equal(await provider.getAddress()); expect(jobInfo.rate).to.equal(JOB_RATE_1); - const amountPaidThisSettle = calcAmountToPay(JOB_RATE_1, DURATION_TO_SETTLE); - const jobBalanceExpected = initialBalance.sub(amountPaidThisSettle); - expect(jobInfo.balance).to.be.closeTo(jobBalanceExpected, 2); - expect(jobCreditBal).to.be.closeTo(jobBalanceExpected, 2); + const jobBalanceExpected = initialBalance.sub(calcAmountToPay(JOB_RATE_1, TWO_MINUTES)); + expect(jobInfo.balance).to.be.within(jobBalanceExpected.sub(JOB_RATE_1), jobBalanceExpected.add(JOB_RATE_1)); - expect(jobInfo.lastSettled).to.be.closeTo(expectedSettledTimestamp, 3); + const lastSettledTimestampExpected = INITIAL_TIMESTAMP + NOTICE_PERIOD + TWO_MINUTES; + expect(jobInfo.lastSettled).to.be.within(lastSettledTimestampExpected - 3, lastSettledTimestampExpected + 3); - const providerBalanceExpected = noticeCostPaidAtOpen.add(amountPaidThisSettle); - expect(await token.balanceOf(await provider.getAddress())).to.be.closeTo(providerBalanceExpected, 2); + // User balance + const userTokenBalanceExpected = SIGNER1_INITIAL_FUND; + expect(await token.balanceOf(await user.getAddress())).to.be.within(userTokenBalanceExpected.sub(JOB_RATE_1), userTokenBalanceExpected.add(JOB_RATE_1)); + const userCreditBalanceExpected = initialBalance.sub(calcAmountToPay(JOB_RATE_1, TWO_MINUTES)); + expect(await creditToken.balanceOf(await user.getAddress())).to.be.within(userCreditBalanceExpected.sub(JOB_RATE_1), userCreditBalanceExpected.add(JOB_RATE_1)); - expect(await token.balanceOf(marketv1.address)).to.equal(0); - expect(await creditToken.balanceOf(marketv1.address)).to.be.closeTo(jobCreditBal, 2); + // Provider balance + const providerBalanceExpected = calcAmountToPay(JOB_RATE_1, TWO_MINUTES).add(calcNoticePeriodCost(JOB_RATE_1)); + expect(await token.balanceOf(await provider.getAddress())).to.be.within(providerBalanceExpected.sub(JOB_RATE_1), providerBalanceExpected.add(JOB_RATE_1)); + + // MarketV1 balance + const marketv1TokenBalanceExpected = initialBalance.sub(calcAmountToPay(JOB_RATE_1, TWO_MINUTES)); + expect(await token.balanceOf(marketv1.address)).to.be.within(marketv1TokenBalanceExpected.sub(JOB_RATE_1), marketv1TokenBalanceExpected.add(JOB_RATE_1)); + const marketv1CreditBalanceExpected = calcAmountToPay(JOB_RATE_1, TWO_MINUTES); + expect(await creditToken.balanceOf(marketv1.address)).to.be.within(marketv1CreditBalanceExpected.sub(JOB_RATE_1), marketv1CreditBalanceExpected.add(JOB_RATE_1)); }); }); }); @@ -753,148 +767,1395 @@ describe("MarketV1", function () { beforeEach(async () => { await token.connect(user).approve(marketv1.address, usdcDeposit); await creditToken.connect(user).approve(marketv1.address, creditDeposit); - await marketv1 + // deposit 10 credit and 40 usdc + const tx = await marketv1 .connect(user) .jobOpen("some metadata", await provider.getAddress(), JOB_RATE_1, usdcDeposit.add(creditDeposit)); - jobOpenActualTimestamp = (await ethers.provider.getBlock('latest')).timestamp; + const receipt = await tx.wait(); + const block = await ethers.provider.getBlock(receipt.blockHash); + JOB_OPENED_TIMESTAMP = block.timestamp; }); describe("CASE1: Settle Job immediately after Job Open", function () { - it("should have balance and lastSettled reflecting initial state after open", async () => { - const jobBeforeSettle = await marketv1.jobs(INITIAL_JOB_INDEX); - const creditBalanceBeforeSettle = await marketv1.jobCreditBalance(INITIAL_JOB_INDEX); - + it("should revert before lastSettled", async () => { await marketv1.connect(user).jobSettle(INITIAL_JOB_INDEX); const jobInfo = await marketv1.jobs(INITIAL_JOB_INDEX); - const creditBalanceAfterSettle = await marketv1.jobCreditBalance(INITIAL_JOB_INDEX); - - expect(jobInfo.balance).to.equal(initialBalance); - expect(jobInfo.lastSettled).to.be.closeTo(jobOpenActualTimestamp, 2); - expect(creditBalanceAfterSettle).to.equal(creditBalanceBeforeSettle); - expect(creditBalanceAfterSettle).to.equal(creditDeposit.sub(calcNoticePeriodCost(JOB_RATE_1))); + expect(jobInfo.lastSettled).to.be.closeTo(INITIAL_TIMESTAMP, 2); + expect(jobInfo.balance).to.equal(initialBalance.sub(calcAmountToPay(JOB_RATE_1, jobInfo.lastSettled.sub(JOB_OPENED_TIMESTAMP).toNumber()))); + expect(await marketv1.jobCreditBalance(INITIAL_JOB_INDEX)).to.equal( + creditDeposit.sub(calcNoticePeriodCost(JOB_RATE_1)).sub(calcAmountToPay(JOB_RATE_1, jobInfo.lastSettled.sub(JOB_OPENED_TIMESTAMP).toNumber())) + ); }); }); describe("CASE2: Settle Job 2 minutes after Job Open", function () { - it("should settle for 2 minutes, using credit then USDC", async () => { - const DURATION_TO_SETTLE = TWO_MINUTES; - const expectedSettledTimestamp = jobOpenActualTimestamp + DURATION_TO_SETTLE; - await time.increaseTo(expectedSettledTimestamp); - - const amountToSettle = calcAmountToPay(JOB_RATE_1, DURATION_TO_SETTLE); - const initialJobCreditBalance = creditDeposit.sub(calcNoticePeriodCost(JOB_RATE_1)); - + it("should revert before lastSettled", async () => { + const TWO_MINUTES = 60 * 2; + await time.increaseTo(INITIAL_TIMESTAMP + TWO_MINUTES); + await marketv1.connect(user).jobSettle(INITIAL_JOB_INDEX); const jobInfo = await marketv1.jobs(INITIAL_JOB_INDEX); - const jobCreditBal = await marketv1.jobCreditBalance(INITIAL_JOB_INDEX); - - expect(jobInfo.lastSettled).to.be.closeTo(expectedSettledTimestamp, 2); - expect(jobInfo.balance).to.equal(initialBalance.sub(amountToSettle)); - let expectedCreditBalanceAfterSettle = initialJobCreditBalance.sub(amountToSettle); - if (expectedCreditBalanceAfterSettle.lt(0)) { - expectedCreditBalanceAfterSettle = BN.from(0); - } - expect(jobCreditBal).to.equal(expectedCreditBalanceAfterSettle); + expect(jobInfo.lastSettled).to.be.within(INITIAL_TIMESTAMP + TWO_MINUTES, INITIAL_TIMESTAMP + TWO_MINUTES + 1); + expect(jobInfo.balance).to.equal(initialBalance.sub(calcAmountToPay(JOB_RATE_1, jobInfo.lastSettled.sub(JOB_OPENED_TIMESTAMP).toNumber()))); + + expect(await marketv1.jobCreditBalance(INITIAL_JOB_INDEX)).to.equal( + creditDeposit.sub(calcNoticePeriodCost(JOB_RATE_1).add(calcAmountToPay(JOB_RATE_1, jobInfo.lastSettled.sub(JOB_OPENED_TIMESTAMP).toNumber()))) + ); }); }); describe("CASE3: Settle Job exactly after notice period", function () { - it("should settle for notice period, using credit then USDC", async () => { - const DURATION_TO_SETTLE = NOTICE_PERIOD; - const expectedSettledTimestamp = jobOpenActualTimestamp + DURATION_TO_SETTLE; - await time.increaseTo(expectedSettledTimestamp); - - const amountToSettle = calcAmountToPay(JOB_RATE_1, DURATION_TO_SETTLE); - const initialJobCreditBalance = creditDeposit.sub(calcNoticePeriodCost(JOB_RATE_1)); - + it("Balance should decrease by NOTICE PERIOD * 2 duration", async () => { + await time.increaseTo(INITIAL_TIMESTAMP + NOTICE_PERIOD); + await marketv1.connect(user).jobSettle(INITIAL_JOB_INDEX); const jobInfo = await marketv1.jobs(INITIAL_JOB_INDEX); - const jobCreditBal = await marketv1.jobCreditBalance(INITIAL_JOB_INDEX); + expect(jobInfo.lastSettled).to.be.within(INITIAL_TIMESTAMP + NOTICE_PERIOD, INITIAL_TIMESTAMP + NOTICE_PERIOD + 1); + expect(jobInfo.balance).to.equal(initialBalance.sub(calcAmountToPay(JOB_RATE_1, jobInfo.lastSettled.sub(JOB_OPENED_TIMESTAMP).toNumber()))); - expect(jobInfo.lastSettled).to.be.closeTo(expectedSettledTimestamp, 2); - expect(jobInfo.balance).to.equal(initialBalance.sub(amountToSettle)); - - let expectedCreditBalanceAfterSettle = initialJobCreditBalance.sub(amountToSettle); - if (expectedCreditBalanceAfterSettle.lt(0)) { - expectedCreditBalanceAfterSettle = BN.from(0); - } - expect(jobCreditBal).to.equal(expectedCreditBalanceAfterSettle); + expect(await marketv1.jobCreditBalance(INITIAL_JOB_INDEX)).to.equal( + creditDeposit.sub(calcNoticePeriodCost(JOB_RATE_1).add(calcAmountToPay(JOB_RATE_1, jobInfo.lastSettled.sub(JOB_OPENED_TIMESTAMP).toNumber()))) + ); }); }); - describe("CASE4: Settle Job 2 minutes after notice period - credit might be used up", function () { - it("should settle, exhausting credit first, then USDC", async () => { - const DURATION_TO_SETTLE = NOTICE_PERIOD + TWO_MINUTES; - const TIME_JOB_SETTLE_TARGET = jobOpenActualTimestamp + DURATION_TO_SETTLE; - await time.increaseTo(TIME_JOB_SETTLE_TARGET); - - const noticeCostPaidAtOpen = calcNoticePeriodCost(JOB_RATE_1); - const initialJobCreditBalance = creditDeposit.sub(noticeCostPaidAtOpen); + describe("CASE4: Settle Job 2 minutes after notice period - only Credit is settled", function () { + it("should settle notice period cost and 2 minutes worth tokens only from Credit", async () => { + const TWO_MINUTES = 60 * 2; + const TIME_JOB_OPEN = (await ethers.provider.getBlock('latest')).timestamp; + await time.increaseTo(TIME_JOB_OPEN + NOTICE_PERIOD + TWO_MINUTES); + const TIME_JOB_SETTLE = (await ethers.provider.getBlock('latest')).timestamp; + const TIME_DIFF = TIME_JOB_SETTLE - TIME_JOB_OPEN; + + const noticePeriodCost = calcNoticePeriodCost(JOB_RATE_1); + // Job Settle await marketv1.connect(user).jobSettle(INITIAL_JOB_INDEX); + /* Job Info After Settle */ const jobInfo = await marketv1.jobs(INITIAL_JOB_INDEX); - const finalJobCreditBalance = await marketv1.jobCreditBalance(INITIAL_JOB_INDEX); - expect(jobInfo.metadata).to.equal("some metadata"); expect(jobInfo.owner).to.equal(await user.getAddress()); expect(jobInfo.provider).to.equal(await provider.getAddress()); expect(jobInfo.rate).to.equal(JOB_RATE_1); - - const amountPaidThisSettle = calcAmountToPay(JOB_RATE_1, DURATION_TO_SETTLE); - const jobBalanceExpected = initialBalance.sub(amountPaidThisSettle); - expect(jobInfo.balance).to.be.closeTo(jobBalanceExpected, 2); - - expect(jobInfo.lastSettled).to.be.closeTo(TIME_JOB_SETTLE_TARGET, 3); - - let expectedFinalCreditBalance = initialJobCreditBalance.sub(amountPaidThisSettle); - if (expectedFinalCreditBalance.lt(0)) { - expectedFinalCreditBalance = BN.from(0); - } - expect(finalJobCreditBalance).to.equal(expectedFinalCreditBalance); - - const providerPaymentThisSettle = amountPaidThisSettle; - const providerBalanceExpected = noticeCostPaidAtOpen.add(providerPaymentThisSettle); - expect(await token.balanceOf(await provider.getAddress())).to.be.closeTo(providerBalanceExpected, 2); - - const expectedMarketV1UsdcBalance = jobBalanceExpected.sub(expectedFinalCreditBalance); - expect(await token.balanceOf(marketv1.address)).to.be.closeTo(expectedMarketV1UsdcBalance, 2); - expect(await creditToken.balanceOf(marketv1.address)).to.equal(expectedFinalCreditBalance); + // Job Balance + const jobBalanceExpected = initialBalance.sub(calcAmountToPay(JOB_RATE_1, TWO_MINUTES)); + expect(jobInfo.balance).to.be.within(jobBalanceExpected.sub(JOB_RATE_1), jobBalanceExpected.add(JOB_RATE_1)); + // Job Last Settled + const lastSettledTimestampExpected = TIME_JOB_OPEN + NOTICE_PERIOD + TWO_MINUTES; + expect(jobInfo.lastSettled).to.be.within(lastSettledTimestampExpected - 3, lastSettledTimestampExpected + 3); + // Job Credit Balance + const amountSettledExpected = calcAmountToPay(JOB_RATE_1, TIME_DIFF); + const jobCreditBalanceExpected = creditDeposit.sub(amountSettledExpected); + expect(await marketv1.jobCreditBalance(INITIAL_JOB_INDEX)).to.be.within(jobCreditBalanceExpected.sub(JOB_RATE_1), jobCreditBalanceExpected.add(JOB_RATE_1)); + + /* User balance */ + // User Token balance + const userTokenBalanceExpected = SIGNER1_INITIAL_FUND.sub(usdcDeposit); + expect(await token.balanceOf(await user.getAddress())).to.be.within(userTokenBalanceExpected.sub(JOB_RATE_1), userTokenBalanceExpected.add(JOB_RATE_1)); + // User Credit balance + const userCreditBalanceExpected = initialBalance.sub(calcAmountToPay(JOB_RATE_1, TWO_MINUTES)); + expect(await creditToken.balanceOf(await user.getAddress())).to.be.within(userCreditBalanceExpected.sub(JOB_RATE_1), userCreditBalanceExpected.add(JOB_RATE_1)); + + /* Provider balance */ + // Provider Token balance + const providerBalanceExpected = calcAmountToPay(JOB_RATE_1, TWO_MINUTES).add(calcNoticePeriodCost(JOB_RATE_1)); + expect(await token.balanceOf(await provider.getAddress())).to.be.within(providerBalanceExpected.sub(JOB_RATE_1), providerBalanceExpected.add(JOB_RATE_1)); + + /* MarketV1 balance */ + // MarketV1 Token balance + const marketv1TokenBalanceExpected = initialBalance.sub(calcAmountToPay(JOB_RATE_1, TWO_MINUTES)); + expect(await token.balanceOf(marketv1.address)).to.be.within(marketv1TokenBalanceExpected.sub(JOB_RATE_1), marketv1TokenBalanceExpected.add(JOB_RATE_1)); + // MarketV1 Credit balance + const marketv1CreditBalanceExpected = calcAmountToPay(JOB_RATE_1, TWO_MINUTES); + expect(await creditToken.balanceOf(marketv1.address)).to.be.within(marketv1CreditBalanceExpected.sub(JOB_RATE_1), marketv1CreditBalanceExpected.add(JOB_RATE_1)); }); }); describe("CASE5: Settle Job 20 minutes after notice period - both Credit and USDC are settled", function () { it("should settle all Credits and some USDC", async () => { - const DURATION_TO_SETTLE = NOTICE_PERIOD + 60 * 20; - const TIME_JOB_SETTLE_TARGET = jobOpenActualTimestamp + DURATION_TO_SETTLE; - await time.increaseTo(TIME_JOB_SETTLE_TARGET); + const TWENTY_MINUTES = 60 * 20; + await time.increaseTo(JOB_OPENED_TIMESTAMP + NOTICE_PERIOD + TWENTY_MINUTES); + const TIME_JOB_SETTLE = (await ethers.provider.getBlock('latest')).timestamp; + const TIME_DIFF = TIME_JOB_SETTLE - JOB_OPENED_TIMESTAMP; - const noticeCostPaidAtOpen = calcNoticePeriodCost(JOB_RATE_1); - const initialJobCreditBalance = creditDeposit.sub(noticeCostPaidAtOpen); + const noticePeriodCost = calcNoticePeriodCost(JOB_RATE_1); + // Job Settle await marketv1.connect(user).jobSettle(INITIAL_JOB_INDEX); + /* Job Info After Settle */ const jobInfo = await marketv1.jobs(INITIAL_JOB_INDEX); - const finalJobCreditBalance = await marketv1.jobCreditBalance(INITIAL_JOB_INDEX); expect(jobInfo.metadata).to.equal("some metadata"); + expect(jobInfo.owner).to.equal(await user.getAddress()); + expect(jobInfo.provider).to.equal(await provider.getAddress()); + expect(jobInfo.rate).to.equal(JOB_RATE_1); + // Job Balance + const jobBalanceExpected = initialBalance.sub(calcAmountToPay(JOB_RATE_1, TWENTY_MINUTES)); + expect(jobInfo.balance).to.be.within(jobBalanceExpected.sub(JOB_RATE_1), jobBalanceExpected.add(JOB_RATE_1)); + // Job Last Settled + const lastSettledTimestampExpected = JOB_OPENED_TIMESTAMP + NOTICE_PERIOD + TWENTY_MINUTES; + expect(jobInfo.lastSettled).to.be.within(lastSettledTimestampExpected - 3, lastSettledTimestampExpected + 3); + // Job Credit Balance + const amountSettledExpected = calcAmountToPay(JOB_RATE_1, TIME_DIFF); + const jobCreditBalanceExpected = creditDeposit.sub(amountSettledExpected); + expect(await marketv1.jobCreditBalance(INITIAL_JOB_INDEX)).to.be.within(jobCreditBalanceExpected.sub(JOB_RATE_1), jobCreditBalanceExpected.add(JOB_RATE_1)); + expect(await marketv1.jobCreditBalance(INITIAL_JOB_INDEX)).to.equal(0); + + /* User balance */ + // User Token balance + const userTokenBalanceExpected = SIGNER1_INITIAL_FUND.sub(usdcDeposit); + expect(await token.balanceOf(await user.getAddress())).to.be.within(userTokenBalanceExpected.sub(JOB_RATE_1), userTokenBalanceExpected.add(JOB_RATE_1)); + // User Credit balance + const userCreditBalanceExpected = initialBalance.sub(calcAmountToPay(JOB_RATE_1, TWO_MINUTES)); + expect(await creditToken.balanceOf(await user.getAddress())).to.be.within(userCreditBalanceExpected.sub(JOB_RATE_1), userCreditBalanceExpected.add(JOB_RATE_1)); + + /* Provider balance */ + // Provider Token balance + const providerBalanceExpected = calcAmountToPay(JOB_RATE_1, TWO_MINUTES).add(calcNoticePeriodCost(JOB_RATE_1)); + expect(await token.balanceOf(await provider.getAddress())).to.be.within(providerBalanceExpected.sub(JOB_RATE_1), providerBalanceExpected.add(JOB_RATE_1)); + + /* MarketV1 balance */ + // MarketV1 Token balance + const marketv1TokenBalanceExpected = initialBalance.sub(calcAmountToPay(JOB_RATE_1, TWO_MINUTES)); + expect(await token.balanceOf(marketv1.address)).to.be.within(marketv1TokenBalanceExpected.sub(JOB_RATE_1), marketv1TokenBalanceExpected.add(JOB_RATE_1)); + // MarketV1 Credit balance + const marketv1CreditBalanceExpected = calcAmountToPay(JOB_RATE_1, TWO_MINUTES); + expect(await creditToken.balanceOf(marketv1.address)).to.be.within(marketv1CreditBalanceExpected.sub(JOB_RATE_1), marketv1CreditBalanceExpected.add(JOB_RATE_1)); + }); + }); + }); + }); + + describe("Job Deposit", function () { + takeSnapshotBeforeAndAfterEveryTest(async () => { }); + + describe("USDC Only", function () { + it("should deposit to job with USDC", async () => { + const initialDeposit = usdc(50); + const initialBalance = initialDeposit.sub(calcNoticePeriodCost(JOB_RATE_1)); + const additionalDepositAmount = usdc(25); + + await marketv1 + .connect(user) + .jobOpen("some metadata", await provider.getAddress(), JOB_RATE_1, initialDeposit); + + // Deposit 25 USDC + await marketv1 + .connect(signers[1]) + .jobDeposit(INITIAL_JOB_INDEX, additionalDepositAmount); + + // Job after deposit + const jobInfo = await marketv1.jobs(INITIAL_JOB_INDEX); + expect(jobInfo.metadata).to.equal("some metadata"); + expect(jobInfo.owner).to.equal(addrs[1]); + expect(jobInfo.provider).to.equal(addrs[2]); + expect(jobInfo.rate).to.equal(JOB_RATE_1); + expect(jobInfo.balance).to.equal(initialBalance.add(additionalDepositAmount)); + expect(jobInfo.lastSettled).to.be.within(INITIAL_TIMESTAMP - 3, INITIAL_TIMESTAMP + 3); + + const userBalanceExpected = SIGNER1_INITIAL_FUND.sub(initialDeposit).sub(additionalDepositAmount); + expect(await token.balanceOf(addrs[1])).to.be.within(userBalanceExpected.sub(JOB_RATE_1), userBalanceExpected.add(JOB_RATE_1)); + + const marketv1BalanceExpected = initialBalance.add(additionalDepositAmount); + expect(await token.balanceOf(marketv1.address)).to.be.within(marketv1BalanceExpected.sub(JOB_RATE_1), marketv1BalanceExpected.add(JOB_RATE_1)); + }); + + it("should revert when depositing to job without enough approved", async () => { + const initialDeposit = usdc(50); + const initialBalance = initialDeposit.sub(calcNoticePeriodCost(JOB_RATE_1)); + const additionalDepositAmount = usdc(25); + + await token.connect(user).approve(marketv1.address, initialDeposit); + await marketv1 + .connect(user) + .jobOpen("some metadata", await provider.getAddress(), JOB_RATE_1, initialDeposit); + + // Deposit 25 USDC + await expect(marketv1 + .connect(user) + .jobDeposit(INITIAL_JOB_INDEX, additionalDepositAmount)).to.be.revertedWith("ERC20: insufficient allowance"); + }); + + it("should revert when depositing to job without enough balance", async () => { + const initialDeposit = SIGNER1_INITIAL_FUND; + const initialBalance = initialDeposit.sub(calcNoticePeriodCost(JOB_RATE_1)); + const additionalDepositAmount = usdc(25); + + // Open Job + await token.connect(user).approve(marketv1.address, SIGNER1_INITIAL_FUND.add(usdc(1000))); + await marketv1 + .connect(user) + .jobOpen("some metadata", await provider.getAddress(), JOB_RATE_1, initialDeposit); + + // Deposit 25 USDC + await expect(marketv1 + .connect(user) + .jobDeposit(INITIAL_JOB_INDEX, additionalDepositAmount)).to.be.revertedWith("ERC20: transfer amount exceeds balance"); + }); + + it("should revert when depositing to never registered job", async () => { + await expect(marketv1 + .connect(user) + .jobDeposit(ethers.utils.hexZeroPad("0x01", 32), 25)).to.be.revertedWith("job not found"); + }); + + it("should revert when depositing to closed job", async () => { + const initialDeposit = usdc(50); + + // Job Open + await token.connect(user).approve(marketv1.address, initialDeposit); + await marketv1 + .connect(user) + .jobOpen("some metadata", await provider.getAddress(), JOB_RATE_1, initialDeposit); + + // Job Close + await marketv1.connect(user).jobClose(INITIAL_JOB_INDEX); + + // Job Deposit + await expect(marketv1 + .connect(signers[1]) + .jobDeposit(INITIAL_JOB_INDEX, 25)).to.be.revertedWith("job not found"); + }); + }); + + describe("Credit Only", function () { + const INITIAL_DEPOSIT_AMOUNT = usdc(50); + const ADDITIONAL_DEPOSIT_AMOUNT = usdc(25); + const NOTICE_PERIOD_COST = calcNoticePeriodCost(JOB_RATE_1); + + it("should deposit to job with Credit", async () => { + const tx_open = await marketv1 + .connect(user) + .jobOpen("some metadata", await provider.getAddress(), JOB_RATE_1, INITIAL_DEPOSIT_AMOUNT); + const receipt_open = await tx_open.wait(); + const block_open = await ethers.provider.getBlock(receipt_open.blockHash); + const jobOpenTs = block_open.timestamp; + + // Deposit 25 Credit + await creditToken.connect(user).approve(marketv1.address, ADDITIONAL_DEPOSIT_AMOUNT); + const tx = await marketv1 + .connect(signers[1]) + .jobDeposit(INITIAL_JOB_INDEX, ADDITIONAL_DEPOSIT_AMOUNT); + const receipt = await tx.wait(); + const block = await ethers.provider.getBlock(receipt.blockHash); + const currentTimestamp = block.timestamp; + // Job after deposit + const jobInfo = await marketv1.jobs(INITIAL_JOB_INDEX); + expect(jobInfo.metadata).to.equal("some metadata"); + expect(jobInfo.owner).to.equal(await user.getAddress()); + expect(jobInfo.provider).to.equal(await provider.getAddress()); + expect(jobInfo.rate).to.equal(JOB_RATE_1); + expect(jobInfo.balance).to.equal(INITIAL_DEPOSIT_AMOUNT.sub(NOTICE_PERIOD_COST.add(calcAmountToPay(JOB_RATE_1, currentTimestamp - jobOpenTs))).add(ADDITIONAL_DEPOSIT_AMOUNT)); + expect(jobInfo.lastSettled).to.equal(currentTimestamp); + expect(jobInfo.lastSettled).to.be.closeTo(jobOpenTs, 3); + + const userBalanceExpected = SIGNER1_INITIAL_FUND.sub(INITIAL_DEPOSIT_AMOUNT).sub(ADDITIONAL_DEPOSIT_AMOUNT); + expect(await token.balanceOf(addrs[1])).to.be.within(userBalanceExpected.sub(JOB_RATE_1), userBalanceExpected.add(JOB_RATE_1)); + + const marketv1BalanceExpected = INITIAL_DEPOSIT_AMOUNT.add(ADDITIONAL_DEPOSIT_AMOUNT); + expect(await token.balanceOf(marketv1.address)).to.be.within(marketv1BalanceExpected.sub(JOB_RATE_1), marketv1BalanceExpected.add(JOB_RATE_1)); + }); + + it("should revert when depositing to job without approving both Credit and USDC", async () => { + const additionalDepositAmount = usdc(25); + + await token.connect(user).approve(marketv1.address, INITIAL_DEPOSIT_AMOUNT); + await marketv1 + .connect(user) + .jobOpen("some metadata", await provider.getAddress(), JOB_RATE_1, INITIAL_DEPOSIT_AMOUNT); + + // Deposit without approving Credit + await expect(marketv1 + .connect(user) + .jobDeposit(INITIAL_JOB_INDEX, ADDITIONAL_DEPOSIT_AMOUNT)).to.be.revertedWith("ERC20: insufficient allowance"); + }); + + it("should deposit USDC when Credit credit balance is not enough", async () => { + const initialDeposit = SIGNER1_INITIAL_FUND; + + // Open Job + await token.connect(user2).approve(marketv1.address, SIGNER1_INITIAL_FUND.add(usdc(1000))); + await marketv1 + .connect(user2) + .jobOpen("some metadata", await provider.getAddress(), JOB_RATE_1, initialDeposit); + + // Approve 25 Credit without having enough Credit balance + await creditToken.connect(user2).approve(marketv1.address, ADDITIONAL_DEPOSIT_AMOUNT); + await expect(marketv1 + .connect(user2) + .jobDeposit(INITIAL_JOB_INDEX, ADDITIONAL_DEPOSIT_AMOUNT)).to.be.revertedWith("ERC20: transfer amount exceeds balance"); + + const jobInfo = await marketv1.jobs(INITIAL_JOB_INDEX); + const creditBalance = await creditToken.balanceOf(await user2.getAddress()); + expect(jobInfo.balance).to.equal(initialDeposit.sub(NOTICE_PERIOD_COST)); + expect(creditBalance).to.equal(0); + }); + + it("should revert when depositing to never registered job", async () => { + await creditToken.connect(user).approve(marketv1.address, INITIAL_DEPOSIT_AMOUNT); + await expect(marketv1 + .connect(user) + .jobDeposit(INITIAL_JOB_INDEX, ADDITIONAL_DEPOSIT_AMOUNT)).to.be.revertedWith("job not found"); + }); + + it("should revert when depositing to closed job", async () => { + const initialDeposit = usdc(50); + + // Job Open + await token.connect(user).approve(marketv1.address, initialDeposit); + await marketv1 + .connect(user) + .jobOpen("some metadata", await provider.getAddress(), JOB_RATE_1, initialDeposit); + + // Job Close + await marketv1.connect(user).jobClose(INITIAL_JOB_INDEX); + + // Job Deposit + await creditToken.connect(user).approve(marketv1.address, ADDITIONAL_DEPOSIT_AMOUNT); + await expect(marketv1 + .connect(signers[1]) + .jobDeposit(INITIAL_JOB_INDEX, 25)).to.be.revertedWith("job not found"); + }); + }); + + describe("Both Credit and USDC", function () { + const INITIAL_DEPOSIT_AMOUNT = usdc(50); + const NOTICE_PERIOD_COST = calcNoticePeriodCost(JOB_RATE_1); + const TOTAL_ADITIONAL_DEPOSIT_AMOUNT = usdc(40); + const ADDITIONAL_CREDIT_DEPOSIT_AMOUNT = usdc(30); + const ADDITIONAL_USDC_DEPOSIT_AMOUNT = usdc(10); + const TOTAL_DEPOSIT_AMOUNT = INITIAL_DEPOSIT_AMOUNT.add(ADDITIONAL_USDC_DEPOSIT_AMOUNT).add(ADDITIONAL_CREDIT_DEPOSIT_AMOUNT); + + it("should deposit 10 USDC and 30 Credit", async () => { + await marketv1 + .connect(user) + .jobOpen("some metadata", await provider.getAddress(), JOB_RATE_1, INITIAL_DEPOSIT_AMOUNT); + expect((await marketv1.jobs(INITIAL_JOB_INDEX)).balance).to.equal(INITIAL_DEPOSIT_AMOUNT.sub(NOTICE_PERIOD_COST)); + + // Deposit 30 Credit and 10 USDC + await creditToken.connect(user).approve(marketv1.address, ADDITIONAL_CREDIT_DEPOSIT_AMOUNT); + await marketv1 + .connect(user) + .jobDeposit(INITIAL_JOB_INDEX, TOTAL_ADITIONAL_DEPOSIT_AMOUNT); + + const currentTimestamp = (await ethers.provider.getBlock('latest')).timestamp; + + // Job after deposit + const jobInfo = await marketv1.jobs(INITIAL_JOB_INDEX); + const creditBalance = await marketv1.jobCreditBalance(INITIAL_JOB_INDEX); + expect(jobInfo.metadata).to.equal("some metadata"); + expect(jobInfo.owner).to.equal(await user.getAddress()); + expect(jobInfo.provider).to.equal(await provider.getAddress()); + expect(jobInfo.rate).to.equal(JOB_RATE_1); + expect(jobInfo.balance).to.equal(TOTAL_DEPOSIT_AMOUNT.sub(NOTICE_PERIOD_COST)); + expect(jobInfo.lastSettled).to.equal(currentTimestamp); + expect(creditBalance).to.equal(ADDITIONAL_CREDIT_DEPOSIT_AMOUNT); + + // User Balance + const userBalanceExpected = SIGNER1_INITIAL_FUND.sub(INITIAL_DEPOSIT_AMOUNT).sub(ADDITIONAL_USDC_DEPOSIT_AMOUNT).sub(ADDITIONAL_CREDIT_DEPOSIT_AMOUNT); + expect(await token.balanceOf(addrs[1])).to.be.within(userBalanceExpected.sub(JOB_RATE_1), userBalanceExpected.add(JOB_RATE_1)); + + // MarketV1 Balance + const marketv1BalanceExpected = TOTAL_DEPOSIT_AMOUNT.sub(NOTICE_PERIOD_COST); + expect(await token.balanceOf(marketv1.address)).to.be.within(marketv1BalanceExpected.sub(JOB_RATE_1), marketv1BalanceExpected.add(JOB_RATE_1)); + }); + + it("should revert when depositing to job without enough USDC approved", async () => { + await marketv1 + .connect(user) + .jobOpen("some metadata", await provider.getAddress(), JOB_RATE_1, INITIAL_DEPOSIT_AMOUNT); + + // Deposit 30 Credit and 1000 USDC + await creditToken.connect(user).approve(marketv1.address, ADDITIONAL_CREDIT_DEPOSIT_AMOUNT); + await expect(marketv1 + .connect(user) + .jobDeposit(INITIAL_JOB_INDEX, ADDITIONAL_CREDIT_DEPOSIT_AMOUNT.add(SIGNER1_INITIAL_FUND))).to.be.revertedWith("ERC20: insufficient allowance"); + }); + + it("should revert when user does not have enough USDC balance", async () => { + await marketv1 + .connect(user) + .jobOpen("some metadata", await provider.getAddress(), JOB_RATE_1, INITIAL_DEPOSIT_AMOUNT); + + // Deposit 30 Credit and 1000 USDC + await creditToken.connect(user).approve(marketv1.address, ADDITIONAL_CREDIT_DEPOSIT_AMOUNT); + await token.connect(user).approve(marketv1.address, SIGNER1_INITIAL_FUND); + await expect(marketv1 + .connect(user) + .jobDeposit(INITIAL_JOB_INDEX, ADDITIONAL_CREDIT_DEPOSIT_AMOUNT.add(SIGNER1_INITIAL_FUND))).to.be.revertedWith("ERC20: transfer amount exceeds balance"); + }); + + it("should revert when balance is below notice period cost", async () => { + // Open Job + await token.connect(user).approve(marketv1.address, SIGNER1_INITIAL_FUND.add(usdc(1000))); + await marketv1 + .connect(user) + .jobOpen("some metadata", await provider.getAddress(), JOB_RATE_1, SIGNER1_INITIAL_FUND); + + // 100_000 seconds passed (spend 1000 usdc) + await time.increaseTo(INITIAL_TIMESTAMP + 100_000); + + // Deposit 25 USDC + await expect(marketv1 + .connect(user) + .jobDeposit(INITIAL_JOB_INDEX, usdc(25))).to.be.revertedWith("insufficient funds to deposit"); + }); + }); + }); + + describe("Job Withdraw", function () { + const TWO_MINUTES = 60 * 2; + const SEVEN_MINUTES = 60 * 7; + + takeSnapshotBeforeAndAfterEveryTest(async () => { }); + + describe("USDC Only", function () { + const INITIAL_DEPOSIT_AMOUNT = usdc(50); + const NOTICE_PERIOD_COST = calcNoticePeriodCost(JOB_RATE_1); + const TOTAL_WITHDRAW_AMOUNT = usdc(10); + + beforeEach(async () => { + // Deposit 50 USDC + await token.connect(user).approve(marketv1.address, INITIAL_DEPOSIT_AMOUNT); + await marketv1 + .connect(user) + .jobOpen("some metadata", await provider.getAddress(), JOB_RATE_1, INITIAL_DEPOSIT_AMOUNT); + }); + + it("should withdraw from job immediately", async () => { + const withdrawAmount = usdc(10); + + // Job Withdraw + await marketv1 + .connect(user) + .jobWithdraw(INITIAL_JOB_INDEX, withdrawAmount); + + const currentTimestamp = (await ethers.provider.getBlock('latest')).timestamp; + + let jobInfo = await marketv1.jobs(INITIAL_JOB_INDEX); + expect(jobInfo.metadata).to.equal("some metadata"); + expect(jobInfo.owner).to.equal(await user.getAddress()); + expect(jobInfo.provider).to.equal(await provider.getAddress()); + expect(jobInfo.rate).to.equal(JOB_RATE_1); + expect(jobInfo.balance).to.equal(INITIAL_DEPOSIT_AMOUNT.sub(NOTICE_PERIOD_COST).sub(withdrawAmount)); + expect(jobInfo.lastSettled).to.equal(currentTimestamp); + + expect(await token.balanceOf(await user.getAddress())).to.equal(SIGNER1_INITIAL_FUND.sub(INITIAL_DEPOSIT_AMOUNT).add(withdrawAmount)); + expect(await token.balanceOf(await provider.getAddress())).to.equal(NOTICE_PERIOD_COST); + expect(await token.balanceOf(marketv1.address)).to.equal(INITIAL_DEPOSIT_AMOUNT.sub(NOTICE_PERIOD_COST).sub(withdrawAmount)); + }); + + it("should withdraw from job before lastSettled", async () => { + + // 2 minutes passed + await time.increaseTo(INITIAL_TIMESTAMP + TWO_MINUTES); + + const providerBalanceBefore = await token.balanceOf(await provider.getAddress()); + + // Job Withdraw + await marketv1 + .connect(user) + .jobWithdraw(INITIAL_JOB_INDEX, TOTAL_WITHDRAW_AMOUNT); // withdraw 10 USDC + + const SETTLED_AMOUNT = calcAmountToPay(JOB_RATE_1, TWO_MINUTES); + + // Job info after Withdrawal + let jobInfo = await marketv1.jobs(INITIAL_JOB_INDEX); + expect(jobInfo.metadata).to.equal("some metadata"); + expect(jobInfo.owner).to.equal(await user.getAddress()); + expect(jobInfo.provider).to.equal(await provider.getAddress()); + expect(jobInfo.rate).to.equal(JOB_RATE_1); + const jobBalanceExpected = INITIAL_DEPOSIT_AMOUNT.sub(NOTICE_PERIOD_COST).sub(SETTLED_AMOUNT).sub(TOTAL_WITHDRAW_AMOUNT); + expect(jobInfo.balance).to.be.within(jobBalanceExpected.sub(JOB_RATE_1), jobBalanceExpected.add(JOB_RATE_1)); + expect(jobInfo.lastSettled).to.be.within(INITIAL_TIMESTAMP + TWO_MINUTES - 3, INITIAL_TIMESTAMP + TWO_MINUTES + 3); + + // Check User USDC balance + const userUSDCBalanceExpected = SIGNER1_INITIAL_FUND.sub(INITIAL_DEPOSIT_AMOUNT).add(TOTAL_WITHDRAW_AMOUNT); + expect(await token.balanceOf(await user.getAddress())).to.be.within(userUSDCBalanceExpected.sub(JOB_RATE_1), userUSDCBalanceExpected.add(JOB_RATE_1)); + + // Check Provider USDC balance + const providerUSDCBalanceExpected = NOTICE_PERIOD_COST.add(SETTLED_AMOUNT); + expect(await token.balanceOf(await provider.getAddress())).to.be.within(providerUSDCBalanceExpected.sub(JOB_RATE_1), providerUSDCBalanceExpected.add(JOB_RATE_1)); + + // Check MarketV1 USDC balance + const marketv1BalanceExpected = INITIAL_DEPOSIT_AMOUNT.sub(NOTICE_PERIOD_COST).sub(SETTLED_AMOUNT); + expect(await token.balanceOf(marketv1.address)).to.be.within(marketv1BalanceExpected.sub(JOB_RATE_1), marketv1BalanceExpected.add(JOB_RATE_1)); + }); + + it("should withdraw from job after lastSettled with settlement", async () => { + const settledAmountExpected = calcAmountToPay(JOB_RATE_1, TWO_MINUTES); + + // 7 minutes passed after Job Open + await time.increaseTo(INITIAL_TIMESTAMP + NOTICE_PERIOD + TWO_MINUTES); + await marketv1 + .connect(user) + .jobWithdraw(INITIAL_JOB_INDEX, TOTAL_WITHDRAW_AMOUNT); + + expect(await token.balanceOf(await user.getAddress())).to.equal(SIGNER1_INITIAL_FUND.sub(INITIAL_DEPOSIT_AMOUNT).add(TOTAL_WITHDRAW_AMOUNT)); + + const providerBalanceExpected = calcNoticePeriodCost(JOB_RATE_1).add(settledAmountExpected); + expect(await token.balanceOf(await provider.getAddress())).to.be.within(providerBalanceExpected.sub(JOB_RATE_1), providerBalanceExpected.add(JOB_RATE_1)); + + const marketv1BalanceExpected = INITIAL_DEPOSIT_AMOUNT.sub(NOTICE_PERIOD_COST).sub(settledAmountExpected); + expect(await token.balanceOf(marketv1.address)).to.be.within(marketv1BalanceExpected.sub(JOB_RATE_1), marketv1BalanceExpected.add(JOB_RATE_1)); + }); + + it("should revert when withdrawing from non existent job", async () => { + const max_uint256_bytes32 = ethers.utils.hexZeroPad(ethers.constants.MaxUint256.toHexString(), 32); + + await expect(marketv1 + .connect(user) + .jobWithdraw(max_uint256_bytes32, usdc(100))).to.be.revertedWith("only job owner"); + }); + + it("should revert when withdrawing from third party job", async () => { + await expect(marketv1 + .connect(signers[3]) // neither owner nor provider + .jobWithdraw(INITIAL_JOB_INDEX, usdc(100))).to.be.revertedWith("only job owner"); + }); + + it("should revert when balance is below notice period cost", async () => { + // deposited 50 USDC + // 0.01 USDC/s + // notice period cost: 0.01 * 300 = 3 USDC + // 47 USDC left + + await time.increaseTo(INITIAL_TIMESTAMP + NOTICE_PERIOD + 4500); // spend 45 USDC (300 + 4500 seconds passed) + + await expect(marketv1 + .connect(user) + .jobWithdraw(INITIAL_JOB_INDEX, usdc(1))).to.be.revertedWith("insufficient funds to withdraw"); + }); + + it("should revert when withdrawal request amount exceeds max withdrawable amount", async () => { + // Current balance: 47 USDC + + await expect(marketv1 + .connect(user) + .jobWithdraw(INITIAL_JOB_INDEX, usdc(48))).to.be.revertedWith("withdrawal amount exceeds job balance"); + }); + }); + + describe("Credit Only", function () { + const INITIAL_DEPOSIT_AMOUNT = usdc(50); + const NOTICE_PERIOD_COST = calcNoticePeriodCost(JOB_RATE_1); + const CREDIT_WITHDRAW_AMOUNT = usdc(10); + const TOTAL_WITHDRAW_AMOUNT = usdc(10); + + beforeEach(async () => { + // Deposit 50 Credit + await creditToken.connect(user).approve(marketv1.address, INITIAL_DEPOSIT_AMOUNT); + await marketv1.connect(user).jobOpen("some metadata", await provider.getAddress(), JOB_RATE_1, INITIAL_DEPOSIT_AMOUNT); + }); + + it("should withdraw from job immediately", async () => { + // Job Withdraw + await marketv1 + .connect(user) + .jobWithdraw(INITIAL_JOB_INDEX, CREDIT_WITHDRAW_AMOUNT); // withdraw 10 Credit + + const jobInfo = await marketv1.jobs(INITIAL_JOB_INDEX); + const jobCreditBalance = await marketv1.jobCreditBalance(INITIAL_JOB_INDEX); + const currentTimestamp = (await ethers.provider.getBlock('latest')).timestamp; + + expect(jobInfo.metadata).to.equal("some metadata"); + expect(jobInfo.owner).to.equal(await user.getAddress()); + expect(jobInfo.provider).to.equal(await provider.getAddress()); + expect(jobInfo.rate).to.equal(JOB_RATE_1); + expect(jobInfo.balance).to.equal(INITIAL_DEPOSIT_AMOUNT.sub(NOTICE_PERIOD_COST).sub(CREDIT_WITHDRAW_AMOUNT)); + expect(jobInfo.lastSettled).to.equal(currentTimestamp); + + expect(jobCreditBalance).to.equal(INITIAL_DEPOSIT_AMOUNT.sub(NOTICE_PERIOD_COST).sub(CREDIT_WITHDRAW_AMOUNT)); + + // User Balance + const userCreditBalanceExpected = SIGNER1_INITIAL_FUND.sub(INITIAL_DEPOSIT_AMOUNT).add(CREDIT_WITHDRAW_AMOUNT); + expect(await creditToken.balanceOf(await user.getAddress())).to.equal(userCreditBalanceExpected); + + // Provider Balance + const providerCreditBalanceExpected = 0; + const providerUSDCBalanceExpected = NOTICE_PERIOD_COST; + expect(await creditToken.balanceOf(await provider.getAddress())).to.equal(providerCreditBalanceExpected); + expect(await token.balanceOf(await provider.getAddress())).to.equal(providerUSDCBalanceExpected); + + const marketv1CreditBalanceExpected = INITIAL_DEPOSIT_AMOUNT.sub(NOTICE_PERIOD_COST).sub(CREDIT_WITHDRAW_AMOUNT); + expect(await creditToken.balanceOf(marketv1.address)).to.equal(marketv1CreditBalanceExpected); + }); + + it("should withdraw from job before lastSettled", async () => { + // 2 minutes passed + await time.increaseTo(INITIAL_TIMESTAMP + TWO_MINUTES); + const SETTLED_AMOUNT = calcAmountToPay(JOB_RATE_1, TWO_MINUTES); + + // Job Withdraw + await marketv1 + .connect(user) + .jobWithdraw(INITIAL_JOB_INDEX, CREDIT_WITHDRAW_AMOUNT); // withdraw 10 Credit + + // Job info after Withdrawal + let jobInfo = await marketv1.jobs(INITIAL_JOB_INDEX); + const jobCreditBalance = await marketv1.jobCreditBalance(INITIAL_JOB_INDEX); + expect(jobInfo.metadata).to.equal("some metadata"); + expect(jobInfo.owner).to.equal(await user.getAddress()); + expect(jobInfo.provider).to.equal(await provider.getAddress()); + expect(jobInfo.rate).to.equal(JOB_RATE_1); + const jobBalanceExpected = INITIAL_DEPOSIT_AMOUNT.sub(NOTICE_PERIOD_COST).sub(SETTLED_AMOUNT).sub(TOTAL_WITHDRAW_AMOUNT); + expect(jobInfo.balance).to.be.within(jobBalanceExpected.sub(JOB_RATE_1), jobBalanceExpected.add(JOB_RATE_1)); + expect(jobInfo.lastSettled).to.be.within(INITIAL_TIMESTAMP + TWO_MINUTES - 3, INITIAL_TIMESTAMP + TWO_MINUTES + 3); + + const jobCreditBalanceExpected = INITIAL_DEPOSIT_AMOUNT.sub(NOTICE_PERIOD_COST).sub(SETTLED_AMOUNT).sub(TOTAL_WITHDRAW_AMOUNT); + expect(jobCreditBalance).to.be.within(jobCreditBalanceExpected.sub(JOB_RATE_1), jobCreditBalanceExpected.add(JOB_RATE_1)); + + // User Balance + const userCreditBalanceExpected = SIGNER1_INITIAL_FUND.sub(INITIAL_DEPOSIT_AMOUNT).add(TOTAL_WITHDRAW_AMOUNT); + const userUSDCBalanceExpected = 0; + expect(await creditToken.balanceOf(await user.getAddress())).to.equal(userCreditBalanceExpected); + expect(await token.balanceOf(await user.getAddress())).to.equal(SIGNER1_INITIAL_FUND); + + // Provider Balance + const providerCreditBalanceExpected = 0; + const providerUSDCBalanceExpected = NOTICE_PERIOD_COST.add(SETTLED_AMOUNT); + expect(await creditToken.balanceOf(await provider.getAddress())).to.equal(providerCreditBalanceExpected); + expect(await token.balanceOf(await provider.getAddress())).to.be.within(providerUSDCBalanceExpected.sub(JOB_RATE_1), providerUSDCBalanceExpected.add(JOB_RATE_1)); + + // Check MarketV1 USDC balance + const marketv1CreditBalanceExpected = INITIAL_DEPOSIT_AMOUNT.sub(NOTICE_PERIOD_COST).sub(SETTLED_AMOUNT).sub(TOTAL_WITHDRAW_AMOUNT); + const marketv1USDCBalanceExpected = 0; + expect(await creditToken.balanceOf(marketv1.address)).to.be.within(marketv1CreditBalanceExpected.sub(JOB_RATE_1), marketv1CreditBalanceExpected.add(JOB_RATE_1)); + expect(await token.balanceOf(marketv1.address)).to.equal(marketv1USDCBalanceExpected); + }); + + it("should withdraw from job after lastSettled with settlement", async () => { + const settledAmountExpected = calcAmountToPay(JOB_RATE_1, SEVEN_MINUTES); + + // 7 minutes passed after Job Open + await time.increaseTo(INITIAL_TIMESTAMP + SEVEN_MINUTES); + await marketv1 + .connect(user) + .jobWithdraw(INITIAL_JOB_INDEX, TOTAL_WITHDRAW_AMOUNT); + + + // User Balance + const userCreditBalanceExpected = SIGNER1_INITIAL_FUND.sub(INITIAL_DEPOSIT_AMOUNT).add(TOTAL_WITHDRAW_AMOUNT); + const userUSDCBalanceExpected = SIGNER1_INITIAL_FUND; + expect(await creditToken.balanceOf(await user.getAddress())).to.equal(userCreditBalanceExpected); + expect(await token.balanceOf(await user.getAddress())).to.equal(userUSDCBalanceExpected); + + // Provider Balance + const providerCreditBalanceExpected = 0; + const providerUSDCBalanceExpected = NOTICE_PERIOD_COST.add(settledAmountExpected); + expect(await creditToken.balanceOf(await provider.getAddress())).to.equal(providerCreditBalanceExpected); + expect(await token.balanceOf(await provider.getAddress())).to.be.within(providerUSDCBalanceExpected.sub(JOB_RATE_1), providerUSDCBalanceExpected.add(JOB_RATE_1)); + + // Check MarketV1 Balance + const marketv1CreditBalanceExpected = INITIAL_DEPOSIT_AMOUNT.sub(NOTICE_PERIOD_COST).sub(settledAmountExpected).sub(TOTAL_WITHDRAW_AMOUNT); + const marketv1USDCBalanceExpected = 0; + expect(await creditToken.balanceOf(marketv1.address)).to.be.within(marketv1CreditBalanceExpected.sub(JOB_RATE_1), marketv1CreditBalanceExpected.add(JOB_RATE_1)); + expect(await token.balanceOf(marketv1.address)).to.equal(marketv1USDCBalanceExpected); + }); + }); + + describe("Both Credit and USDC", function () { + const INITIAL_DEPOSIT_AMOUNT = usdc(50); + const INITIAL_CREDIT_DEPOSIT_AMOUNT = usdc(40); + const NOTICE_PERIOD_COST = calcNoticePeriodCost(JOB_RATE_1); + const TOTAL_WITHDRAW_AMOUNT = usdc(20); // 13 USDC + 7 Credit + + beforeEach(async () => { + // Deposit 10 Credit, 40 USDC + await creditToken.connect(user).approve(marketv1.address, INITIAL_CREDIT_DEPOSIT_AMOUNT); + await marketv1.connect(user).jobOpen("some metadata", await provider.getAddress(), JOB_RATE_1, INITIAL_DEPOSIT_AMOUNT); + }); + + it("should withdraw only USDC", async () => { + const userCreditBalanceBefore = await creditToken.balanceOf(await user.getAddress()); + const userUSDCBalanceBefore = await token.balanceOf(await user.getAddress()); + + const USDC_WITHDRAWAL_AMOUNT = usdc(5); + + // Job Withdraw + await marketv1 + .connect(user) + .jobWithdraw(INITIAL_JOB_INDEX, USDC_WITHDRAWAL_AMOUNT); // withdraw 5 usdc + + // Job Info + const jobInfo = await marketv1.jobs(INITIAL_JOB_INDEX); + const jobCreditBalance = await marketv1.jobCreditBalance(INITIAL_JOB_INDEX); + const currentTimestamp = (await ethers.provider.getBlock('latest')).timestamp; + + expect(jobInfo.metadata).to.equal("some metadata"); + expect(jobInfo.owner).to.equal(await user.getAddress()); + expect(jobInfo.provider).to.equal(await provider.getAddress()); + expect(jobInfo.rate).to.equal(JOB_RATE_1); + const jobBalanceExpected = INITIAL_DEPOSIT_AMOUNT.sub(NOTICE_PERIOD_COST).sub(TOTAL_WITHDRAW_AMOUNT); + expect(jobInfo.balance).to.be.within(jobBalanceExpected.sub(JOB_RATE_1), jobBalanceExpected.add(JOB_RATE_1)); + expect(jobInfo.lastSettled).to.equal(currentTimestamp); + const jobCreditBalanceExpected = INITIAL_CREDIT_DEPOSIT_AMOUNT.sub(NOTICE_PERIOD_COST); + expect(jobCreditBalance).to.equal(jobCreditBalanceExpected); + + // User Balance + const userCreditBalanceExpected = userCreditBalanceBefore; + const userUSDCBalanceExpected = userUSDCBalanceBefore.sub(USDC_WITHDRAWAL_AMOUNT); + expect(await creditToken.balanceOf(await user.getAddress())).to.equal(userCreditBalanceExpected); + expect(await token.balanceOf(await user.getAddress())).to.be.within(userUSDCBalanceExpected.sub(JOB_RATE_1), userUSDCBalanceExpected.add(JOB_RATE_1)); + + // Provider Balance + const providerCreditBalanceExpected = 0; + const providerUSDCBalanceExpected = NOTICE_PERIOD_COST; + expect(await creditToken.balanceOf(await provider.getAddress())).to.equal(providerCreditBalanceExpected); + expect(await token.balanceOf(await provider.getAddress())).to.equal(providerUSDCBalanceExpected); + + const marketv1CreditBalanceExpected = INITIAL_CREDIT_DEPOSIT_AMOUNT.sub(NOTICE_PERIOD_COST); + const marketv1USDCBalanceExpected = INITIAL_DEPOSIT_AMOUNT.sub(INITIAL_CREDIT_DEPOSIT_AMOUNT).sub(USDC_WITHDRAWAL_AMOUNT); + expect(await creditToken.balanceOf(marketv1.address)).to.be.within(marketv1CreditBalanceExpected.sub(JOB_RATE_1), marketv1CreditBalanceExpected.add(JOB_RATE_1)); + expect(await token.balanceOf(marketv1.address)).to.be.within(marketv1USDCBalanceExpected.sub(JOB_RATE_1), marketv1USDCBalanceExpected.add(JOB_RATE_1)); + }); + + it("should withdraw both USDC and Credit", async () => { + const userCreditBalanceBefore = await creditToken.balanceOf(await user.getAddress()); + const userUSDCBalanceBefore = await token.balanceOf(await user.getAddress()); + + const withdrawnUSDCAmountExpected = ((await marketv1.jobs(INITIAL_JOB_INDEX)).balance).sub(await marketv1.jobCreditBalance(INITIAL_JOB_INDEX)); + const withdrawnCreditAmountExpected = TOTAL_WITHDRAW_AMOUNT.sub(withdrawnUSDCAmountExpected); + // Job Withdraw + await marketv1 + .connect(user) + .jobWithdraw(INITIAL_JOB_INDEX, TOTAL_WITHDRAW_AMOUNT); // withdraw 20 (13 USDC + 7 Credit) + + const currentTimestamp = (await ethers.provider.getBlock('latest')).timestamp; + + // Job Info + const jobInfo = await marketv1.jobs(INITIAL_JOB_INDEX); + const jobCreditBalance = await marketv1.jobCreditBalance(INITIAL_JOB_INDEX); + expect(jobInfo.metadata).to.equal("some metadata"); + expect(jobInfo.owner).to.equal(await user.getAddress()); + expect(jobInfo.provider).to.equal(await provider.getAddress()); + expect(jobInfo.rate).to.equal(JOB_RATE_1); + const jobBalanceExpected = INITIAL_DEPOSIT_AMOUNT.sub(NOTICE_PERIOD_COST).sub(TOTAL_WITHDRAW_AMOUNT); + expect(jobInfo.balance).to.be.within(jobBalanceExpected.sub(JOB_RATE_1), jobBalanceExpected.add(JOB_RATE_1)); + expect(jobInfo.lastSettled).to.equal(currentTimestamp); + expect(jobCreditBalance).to.equal(INITIAL_CREDIT_DEPOSIT_AMOUNT.sub(NOTICE_PERIOD_COST).sub(withdrawnCreditAmountExpected)); + + // User Balance + const userCreditBalanceExpected = userCreditBalanceBefore.add(withdrawnCreditAmountExpected); + const userUSDCBalanceExpected = userUSDCBalanceBefore.add(withdrawnUSDCAmountExpected); + expect(await creditToken.balanceOf(await user.getAddress())).to.equal(userCreditBalanceExpected); + expect(await token.balanceOf(await user.getAddress())).to.equal(userUSDCBalanceExpected); + + // Provider Balance + const providerCreditBalanceExpected = 0; + const providerUSDCBalanceExpected = NOTICE_PERIOD_COST; + expect(await creditToken.balanceOf(await provider.getAddress())).to.equal(providerCreditBalanceExpected); + expect(await token.balanceOf(await provider.getAddress())).to.equal(providerUSDCBalanceExpected); + + // MarketV1 Balance + const marketv1CreditBalanceExpected = INITIAL_CREDIT_DEPOSIT_AMOUNT.sub(NOTICE_PERIOD_COST).sub(withdrawnCreditAmountExpected); + expect(await creditToken.balanceOf(marketv1.address)).to.equal(marketv1CreditBalanceExpected); + expect(await token.balanceOf(marketv1.address)).to.equal(0); + }); + }); + }); + + describe("Job Revise Rate", function () { + const JOB_LOWER_RATE = BN.from(5).e16().div(10); + const JOB_HIGHER_RATE = BN.from(2).e16(); + + const initialDeposit = usdc(50); + const initialBalance = initialDeposit.sub(calcNoticePeriodCost(JOB_RATE_1)); + + takeSnapshotBeforeAndAfterEveryTest(async () => { }); + + beforeEach(async () => { + await token.connect(user).approve(marketv1.address, initialDeposit); + await marketv1 + .connect(user) + .jobOpen("some metadata", await provider.getAddress(), JOB_RATE_1, initialDeposit); + }); + + it("should revise rate higher", async () => { + const currentTimestamp = (await ethers.provider.getBlock('latest')).timestamp; + + await marketv1 + .connect(user) + .jobReviseRate(INITIAL_JOB_INDEX, JOB_HIGHER_RATE); + + const jobInfo = await marketv1.jobs(INITIAL_JOB_INDEX); + expect(jobInfo.rate).to.equal(JOB_HIGHER_RATE); + expect(jobInfo.balance).to.equal(initialBalance.sub(calcNoticePeriodCost(JOB_HIGHER_RATE.sub(JOB_RATE_1)))); + + expect(jobInfo.lastSettled).to.equal(currentTimestamp ); + expect(await token.balanceOf(await user.getAddress())).to.equal(SIGNER1_INITIAL_FUND.sub(initialDeposit)); + expect(await token.balanceOf(await provider.getAddress())).to.equal(calcNoticePeriodCost(JOB_HIGHER_RATE)); + expect(await token.balanceOf(marketv1.address)).to.equal(initialBalance.sub(calcNoticePeriodCost(JOB_HIGHER_RATE.sub(JOB_RATE_1)))); + }); + + it("should revise rate lower", async () => { + const currentTimestamp = (await ethers.provider.getBlock('latest')).timestamp; + + await marketv1 + .connect(user) + .jobReviseRate(INITIAL_JOB_INDEX, JOB_LOWER_RATE); + + const jobInfo = await marketv1.jobs(INITIAL_JOB_INDEX); + expect(jobInfo.rate).to.equal(JOB_LOWER_RATE); + expect(jobInfo.balance).to.equal(initialBalance); + expect(jobInfo.lastSettled).to.equal(currentTimestamp ); + expect(await token.balanceOf(await user.getAddress())).to.equal(SIGNER1_INITIAL_FUND.sub(initialDeposit)); + expect(await token.balanceOf(await provider.getAddress())).to.equal(calcNoticePeriodCost(JOB_RATE_1)); + expect(await token.balanceOf(marketv1.address)).to.equal(initialBalance); + }); + + it("should revert when initiating rate revision for non existent job", async () => { + await expect(marketv1 + .connect(user) + .jobReviseRate(ethers.utils.hexZeroPad("0x01", 32), JOB_HIGHER_RATE)).to.be.revertedWith("only job owner"); + }); + + it("should revert when initiating rate revision for third party job", async () => { + await expect(marketv1 + .connect(signers[3]) // neither owner nor provider + .jobReviseRate(INITIAL_JOB_INDEX, JOB_HIGHER_RATE)).to.be.revertedWith("only job owner"); + }); + + const HIGHER_RATE = BN.from(2).e16(); // 0.02 USDC/s + const LOWER_RATE = BN.from(5).e15(); // 0.005 USDC/s + + describe("CASE 1: Revising Rate immediately after job open", function () { + + describe("when rate is higher", function () { + + it("should spend notice period cost only", async () => { + const noticePeriodCostExpected = calcNoticePeriodCost(JOB_RATE_1); + + await marketv1 + .connect(user) + .jobReviseRate(INITIAL_JOB_INDEX, HIGHER_RATE); + + // Job info after Rate Revision + const jobInfo = await marketv1.jobs(INITIAL_JOB_INDEX); + expect(jobInfo.rate).to.equal(HIGHER_RATE); + const jobBalanceExpected = initialBalance.sub(noticePeriodCostExpected); + expect(jobInfo.balance).to.be.within(jobBalanceExpected.sub(JOB_RATE_1), jobBalanceExpected.add(JOB_RATE_1)); + + const userBalanceExpected = SIGNER1_INITIAL_FUND.sub(initialDeposit).sub(noticePeriodCostExpected); + expect(await token.balanceOf(await user.getAddress())).to.be.within(userBalanceExpected.sub(JOB_RATE_1), userBalanceExpected.add(JOB_RATE_1)); + + const providerBalanceExpected = noticePeriodCostExpected; + expect(await token.balanceOf(await provider.getAddress())).to.be.within(providerBalanceExpected.sub(JOB_RATE_1), providerBalanceExpected.add(JOB_RATE_1)); + + const marketv1BalanceExpected = jobBalanceExpected; + expect(await token.balanceOf(marketv1.address)).to.be.within(marketv1BalanceExpected.sub(JOB_RATE_1), marketv1BalanceExpected.add(JOB_RATE_1)); + }); + }); + + describe("when rate is lower", function () { + it("should spend notice period cost only", async () => { + const noticePeriodCostExpected = calcNoticePeriodCost(JOB_RATE_1); + + await marketv1 + .connect(user) + .jobReviseRate(INITIAL_JOB_INDEX, LOWER_RATE); - const amountPaidThisSettle = calcAmountToPay(JOB_RATE_1, DURATION_TO_SETTLE); - const jobBalanceExpected = initialBalance.sub(amountPaidThisSettle); - expect(jobInfo.balance).to.be.closeTo(jobBalanceExpected, 2); + // Job info after Rate Revision + const jobInfo = await marketv1.jobs(INITIAL_JOB_INDEX); + expect(jobInfo.rate).to.equal(LOWER_RATE); + const jobBalanceExpected = initialDeposit.sub(noticePeriodCostExpected); + expect(jobInfo.balance).to.be.within(jobBalanceExpected.sub(JOB_RATE_1), jobBalanceExpected.add(JOB_RATE_1)); + + const userBalanceExpected = SIGNER1_INITIAL_FUND.sub(noticePeriodCostExpected); + expect(await token.balanceOf(await user.getAddress())).to.be.within(userBalanceExpected.sub(JOB_RATE_1), userBalanceExpected.add(JOB_RATE_1)); + + const providerBalanceExpected = noticePeriodCostExpected; + expect(await token.balanceOf(await provider.getAddress())).to.be.within(providerBalanceExpected.sub(JOB_RATE_1), providerBalanceExpected.add(JOB_RATE_1)); + + const marketv1BalanceExpected = jobBalanceExpected; + expect(await token.balanceOf(marketv1.address)).to.be.within(marketv1BalanceExpected.sub(JOB_RATE_1), marketv1BalanceExpected.add(JOB_RATE_1)); + }); + }); + }); + + describe("CASE 2: Revising Rate 2 minutes after job open", function () { + const TWO_MINUTES = 60 * 2; + const SEVEN_MINUTES = 60 * 7; + + describe("when rate is higher", function () { + it("should spend notice period cost + 3 minutes worth tokens with higher rate", async () => { + // 5 min * initial rate + 3 min * higher rate + const usdcSpentExpected = calcNoticePeriodCost(JOB_RATE_1).add(calcAmountToPay(HIGHER_RATE, TWO_MINUTES)); + const initialTimestamp = (await ethers.provider.getBlock('latest')).timestamp; + + await time.increaseTo(INITIAL_TIMESTAMP + TWO_MINUTES); + + await marketv1 + .connect(user) + .jobReviseRate(INITIAL_JOB_INDEX, HIGHER_RATE); + + const jobInfo = await marketv1.jobs(INITIAL_JOB_INDEX); + expect(jobInfo.rate).to.equal(HIGHER_RATE); + + const jobBalanceExpected = initialDeposit.sub(usdcSpentExpected); + expect(jobInfo.balance).to.be.within(jobBalanceExpected.sub(JOB_RATE_1), jobBalanceExpected.add(JOB_RATE_1)); + + const lastSettledTimestampExpected = (await ethers.provider.getBlock('latest')).timestamp ; + expect(jobInfo.lastSettled).to.be.within(lastSettledTimestampExpected - 3, lastSettledTimestampExpected + 3); + + const userBalanceExpected = SIGNER1_INITIAL_FUND.sub(initialDeposit).sub(usdcSpentExpected); + expect(await token.balanceOf(await user.getAddress())).to.be.within(userBalanceExpected.sub(JOB_RATE_1), userBalanceExpected.add(JOB_RATE_1)); + + const providerBalanceExpected = usdcSpentExpected; + expect(await token.balanceOf(await provider.getAddress())).to.be.within(providerBalanceExpected.sub(JOB_RATE_1), providerBalanceExpected.add(JOB_RATE_1)); + + const marketv1BalanceExpected = jobBalanceExpected; + expect(await token.balanceOf(marketv1.address)).to.be.within(marketv1BalanceExpected.sub(JOB_RATE_1), marketv1BalanceExpected.add(JOB_RATE_1)); + }); + }); + + describe("when rate is lower", function () { + it("should spend notice period cost + 3 minutes worth tokens with initial rate", async () => { + // 5 min * initial rate + 3 min * initial rate + const usdcSpentExpected = calcNoticePeriodCost(JOB_RATE_1).add(calcAmountToPay(JOB_RATE_1, TWO_MINUTES)); + const initialTimestamp = (await ethers.provider.getBlock('latest')).timestamp; + + await time.increaseTo(INITIAL_TIMESTAMP + TWO_MINUTES); + + await marketv1 + .connect(user) + .jobReviseRate(INITIAL_JOB_INDEX, LOWER_RATE); - expect(jobInfo.lastSettled).to.be.closeTo(TIME_JOB_SETTLE_TARGET, 3); + const jobInfo = await marketv1.jobs(INITIAL_JOB_INDEX); + expect(jobInfo.rate).to.equal(LOWER_RATE); + + const jobBalanceExpected = initialDeposit.sub(usdcSpentExpected); + expect(jobInfo.balance).to.be.within(jobBalanceExpected.sub(JOB_RATE_1), jobBalanceExpected.add(JOB_RATE_1)); + + const lastSettledTimestampExpected = (await ethers.provider.getBlock('latest')).timestamp ; + expect(jobInfo.lastSettled).to.be.within(lastSettledTimestampExpected - 3, lastSettledTimestampExpected + 3); + + const userBalanceExpected = SIGNER1_INITIAL_FUND.sub(initialDeposit).sub(usdcSpentExpected); + expect(await token.balanceOf(await user.getAddress())).to.be.within(userBalanceExpected.sub(JOB_RATE_1), userBalanceExpected.add(JOB_RATE_1)); + + const providerBalanceExpected = usdcSpentExpected; + expect(await token.balanceOf(await provider.getAddress())).to.be.within(providerBalanceExpected.sub(JOB_RATE_1), providerBalanceExpected.add(JOB_RATE_1)); + + const marketv1BalanceExpected = jobBalanceExpected; + expect(await token.balanceOf(marketv1.address)).to.be.within(marketv1BalanceExpected.sub(JOB_RATE_1), marketv1BalanceExpected.add(JOB_RATE_1)); + }); + }); + }); + + describe("CASE 3: Revising Rate exactly after notice period", function () { + const TEN_MINUTES = 60 * 10; + + describe("when rate is higher", function () { + it("should spend 5 minutes worth tokens with initial rate and 5 minutes worth tokens with higher rate", async () => { + const initialTimestamp = (await ethers.provider.getBlock('latest')).timestamp; + const firstNoticePeriodCost = calcNoticePeriodCost(JOB_RATE_1); + const secondNoticePeriodCost = calcNoticePeriodCost(HIGHER_RATE); + + await time.increaseTo(INITIAL_TIMESTAMP + NOTICE_PERIOD); + + await marketv1 + .connect(user) + .jobReviseRate(INITIAL_JOB_INDEX, HIGHER_RATE); + + const jobInfo = await marketv1.jobs(INITIAL_JOB_INDEX); + expect(jobInfo.rate).to.equal(HIGHER_RATE); + + const jobBalanceExpected = initialDeposit.sub(firstNoticePeriodCost).sub(secondNoticePeriodCost); + expect(jobInfo.balance).to.be.within(jobBalanceExpected.sub(JOB_RATE_1), jobBalanceExpected.add(JOB_RATE_1)); + + const lastSettledTimestampExpected = (await ethers.provider.getBlock('latest')).timestamp ; + expect(jobInfo.lastSettled).to.be.within(lastSettledTimestampExpected - 3, lastSettledTimestampExpected + 3); + + const userBalanceExpected = SIGNER1_INITIAL_FUND.sub(initialDeposit); + expect(await token.balanceOf(await user.getAddress())).to.be.within(userBalanceExpected.sub(JOB_RATE_1), userBalanceExpected.add(JOB_RATE_1)); + + const providerBalanceExpected = firstNoticePeriodCost.add(secondNoticePeriodCost); + expect(await token.balanceOf(await provider.getAddress())).to.be.within(providerBalanceExpected.sub(JOB_RATE_1), providerBalanceExpected.add(JOB_RATE_1)); + + const marketv1BalanceExpected = jobBalanceExpected; + expect(await token.balanceOf(marketv1.address)).to.be.within(marketv1BalanceExpected.sub(JOB_RATE_1), marketv1BalanceExpected.add(JOB_RATE_1)); + }); + }); + + describe("when rate is lower", function () { + it("should spend 5 minutes worth tokens with initial rate and 5 minutes worth tokens with initial rate", async () => { + const initialTimestamp = (await ethers.provider.getBlock('latest')).timestamp; + const firstNoticePeriodCost = calcNoticePeriodCost(JOB_RATE_1); + const secondNoticePeriodCost = calcNoticePeriodCost(LOWER_RATE); + + await time.increaseTo(INITIAL_TIMESTAMP + NOTICE_PERIOD); + + await marketv1 + .connect(user) + .jobReviseRate(INITIAL_JOB_INDEX, LOWER_RATE); + + const jobInfo = await marketv1.jobs(INITIAL_JOB_INDEX); + expect(jobInfo.rate).to.equal(LOWER_RATE); + + const jobBalanceExpected = initialDeposit.sub(firstNoticePeriodCost).sub(secondNoticePeriodCost); + expect(jobInfo.balance).to.be.within(jobBalanceExpected.sub(JOB_RATE_1), jobBalanceExpected.add(JOB_RATE_1)); + + const lastSettledTimestampExpected = (await ethers.provider.getBlock('latest')).timestamp ; + expect(jobInfo.lastSettled).to.be.within(lastSettledTimestampExpected - 3, lastSettledTimestampExpected + 3); + + const userBalanceExpected = SIGNER1_INITIAL_FUND.sub(initialDeposit); + expect(await token.balanceOf(await user.getAddress())).to.be.within(userBalanceExpected.sub(JOB_RATE_1), userBalanceExpected.add(JOB_RATE_1)); + + const providerBalanceExpected = firstNoticePeriodCost.add(secondNoticePeriodCost); + expect(await token.balanceOf(await provider.getAddress())).to.be.within(providerBalanceExpected.sub(JOB_RATE_1), providerBalanceExpected.add(JOB_RATE_1)); + + const marketv1BalanceExpected = jobBalanceExpected; + expect(await token.balanceOf(marketv1.address)).to.be.within(marketv1BalanceExpected.sub(JOB_RATE_1), marketv1BalanceExpected.add(JOB_RATE_1)); + }); + }); + }); + + describe("CASE 4: Revising Rate 2 minutes after notice period", function () { + const TWO_MINUTES = 60 * 2; + const TWELVE_MINUTES = 60 * 12; + + describe("when rate is higher", function () { + it("should spend 7 minutes worth tokens with initial rate and 5 minutes worth tokens with higher rate", async () => { + const initialTimestamp = (await ethers.provider.getBlock('latest')).timestamp; + const firstNoticePeriodCost = calcNoticePeriodCost(JOB_RATE_1); + const secondNoticePeriodCost = calcNoticePeriodCost(HIGHER_RATE); + + await time.increaseTo(INITIAL_TIMESTAMP + NOTICE_PERIOD + TWO_MINUTES); + + await marketv1 + .connect(user) + .jobReviseRate(INITIAL_JOB_INDEX, HIGHER_RATE); + + const jobInfo = await marketv1.jobs(INITIAL_JOB_INDEX); + expect(jobInfo.rate).to.equal(HIGHER_RATE); + + const jobBalanceExpected = initialDeposit.sub(firstNoticePeriodCost).sub(secondNoticePeriodCost); + expect(jobInfo.balance).to.be.within(jobBalanceExpected.sub(JOB_RATE_1), jobBalanceExpected.add(JOB_RATE_1)); + + const lastSettledTimestampExpected = (await ethers.provider.getBlock('latest')).timestamp ; + expect(jobInfo.lastSettled).to.be.within(lastSettledTimestampExpected - 3, lastSettledTimestampExpected + 3); - expect(finalJobCreditBalance).to.equal(0); + const userBalanceExpected = SIGNER1_INITIAL_FUND.sub(initialDeposit); + expect(await token.balanceOf(await user.getAddress())).to.be.within(userBalanceExpected.sub(JOB_RATE_1), userBalanceExpected.add(JOB_RATE_1)); + + const providerBalanceExpected = firstNoticePeriodCost.add(secondNoticePeriodCost); + expect(await token.balanceOf(await provider.getAddress())).to.be.within(providerBalanceExpected.sub(JOB_RATE_1), providerBalanceExpected.add(JOB_RATE_1)); + + const marketv1BalanceExpected = jobBalanceExpected; + expect(await token.balanceOf(marketv1.address)).to.be.within(marketv1BalanceExpected.sub(JOB_RATE_1), marketv1BalanceExpected.add(JOB_RATE_1)); + }); + }); + + describe("when rate is lower", function () { + it("should spend 7 minutes worth tokens with initial rate and 5 minutes worth tokens with initial rate", async () => { + const initialTimestamp = (await ethers.provider.getBlock('latest')).timestamp; + const firstNoticePeriodCost = calcNoticePeriodCost(JOB_RATE_1); + const secondNoticePeriodCost = calcNoticePeriodCost(JOB_RATE_1); + + await time.increaseTo(INITIAL_TIMESTAMP + NOTICE_PERIOD + TWO_MINUTES); + + await marketv1 + .connect(user) + .jobReviseRate(INITIAL_JOB_INDEX, LOWER_RATE); + + const jobInfo = await marketv1.jobs(INITIAL_JOB_INDEX); + expect(jobInfo.rate).to.equal(LOWER_RATE); + + const jobBalanceExpected = initialDeposit.sub(firstNoticePeriodCost).sub(secondNoticePeriodCost); + expect(jobInfo.balance).to.be.within(jobBalanceExpected.sub(JOB_RATE_1), jobBalanceExpected.add(JOB_RATE_1)); + + const lastSettledTimestampExpected = (await ethers.provider.getBlock('latest')).timestamp ; + expect(jobInfo.lastSettled).to.be.within(lastSettledTimestampExpected - 3, lastSettledTimestampExpected + 3); + + const userBalanceExpected = SIGNER1_INITIAL_FUND.sub(initialDeposit); + expect(await token.balanceOf(await user.getAddress())).to.be.within(userBalanceExpected.sub(JOB_RATE_1), userBalanceExpected.add(JOB_RATE_1)); + + const providerBalanceExpected = firstNoticePeriodCost.add(secondNoticePeriodCost); + expect(await token.balanceOf(await provider.getAddress())).to.be.within(providerBalanceExpected.sub(JOB_RATE_1), providerBalanceExpected.add(JOB_RATE_1)); + + const marketv1BalanceExpected = jobBalanceExpected; + expect(await token.balanceOf(marketv1.address)).to.be.within(marketv1BalanceExpected.sub(JOB_RATE_1), marketv1BalanceExpected.add(JOB_RATE_1)); + }); + }); + }); + }); + + describe("Job Close", function () { + const initialDeposit = usdc(50); + const initialBalance = initialDeposit.sub(calcNoticePeriodCost(JOB_RATE_1)); + + takeSnapshotBeforeAndAfterEveryTest(async () => { }); + + describe("USDC Only", function () { + beforeEach(async () => { + await token.connect(user).approve(marketv1.address, initialDeposit); + await marketv1 + .connect(user) + .jobOpen("some metadata", await provider.getAddress(), JOB_RATE_1, initialDeposit); + }); + + it("should close job", async () => { + // Job Close + await marketv1 + .connect(user) + .jobClose(INITIAL_JOB_INDEX); // here, user should get back (initial deposit - notice period cost) + + const jobInfo = await marketv1.jobs(INITIAL_JOB_INDEX); + expect(jobInfo.metadata).to.equal(""); + expect(jobInfo.owner).to.equal(ethers.constants.AddressZero); + expect(jobInfo.provider).to.equal(ethers.constants.AddressZero); + expect(jobInfo.rate).to.equal(0); + expect(jobInfo.balance).to.equal(0); + expect(jobInfo.lastSettled).to.equal(0); + + const userBalanceExpected = SIGNER1_INITIAL_FUND.sub(initialDeposit).sub(calcNoticePeriodCost(JOB_RATE_1)); + expect(await token.balanceOf(await user.getAddress())).to.be.within(userBalanceExpected.sub(JOB_RATE_1), userBalanceExpected.add(JOB_RATE_1)); + }); + + it("should revert when closing non existent job", async () => { + await expect(marketv1 + .connect(user) + .jobClose(ethers.utils.hexZeroPad("0x01", 32))).to.be.revertedWith("only job owner"); + }); + + it("should revert when closing third party job", async () => { + await expect(marketv1 + .connect(signers[3]) // neither owner nor provider + .jobClose(INITIAL_JOB_INDEX)).to.be.revertedWith("only job owner"); + }); + + describe("Scenario 1: Closing Job immediately after opening", function () { + it("should spend notice period cost only", async () => { + await marketv1 + .connect(user) + .jobClose(INITIAL_JOB_INDEX); + + const noticePeriodCostExpected = calcNoticePeriodCost(JOB_RATE_1); + + // user balance after = initial fund - notice period cost + expect(await token.balanceOf(await user.getAddress())).to.equal(SIGNER1_INITIAL_FUND.sub(noticePeriodCostExpected)); + // provider balance after = notice period cost + expect(await token.balanceOf(await provider.getAddress())).to.equal(noticePeriodCostExpected); + // marketv1 balance after = 0 + expect(await token.balanceOf(marketv1.address)).to.equal(0); + }); + }); + + describe("Scenario 2: Closing Job 2 minutes after opening (before notice period)", function () { + it("should spend notice period cost only", async () => { + const TWO_MINUTES = 60 * 2; + + await time.increaseTo(INITIAL_TIMESTAMP + TWO_MINUTES); + + await marketv1 + .connect(user) + .jobClose(INITIAL_JOB_INDEX); + + const noticePeriodCostExpected = calcNoticePeriodCost(JOB_RATE_1); + + // user balance after = initial fund - 3 minutes worth tokens - notice period cost + const userBalanceExpected = SIGNER1_INITIAL_FUND.sub(usdc(TWO_MINUTES)).sub(noticePeriodCostExpected); + expect(await token.balanceOf(await user.getAddress())).to.be.within(userBalanceExpected.sub(JOB_RATE_1), userBalanceExpected.add(JOB_RATE_1)); + + // provider balance after = 2 minutes worth tokens + notice period cost + const providerBalanceExpected = usdc(TWO_MINUTES).add(noticePeriodCostExpected); + expect(await token.balanceOf(await provider.getAddress())).to.be.within(providerBalanceExpected.sub(JOB_RATE_1), providerBalanceExpected.add(JOB_RATE_1)); + + // marketv1 balance after = 0 + expect(await token.balanceOf(marketv1.address)).to.equal(0); + }); + }); + + describe("Scenario 3: Closing Job exactly after notice period", function () { + it("should spend 10 minutes worth tokens", async () => { + const usdcSpentExpected = calcAmountToPay(JOB_RATE_1, FIVE_MINUTES).add(calcNoticePeriodCost(JOB_RATE_1)); + + await time.increaseTo(INITIAL_TIMESTAMP + NOTICE_PERIOD); + + await marketv1 + .connect(user) + .jobClose(INITIAL_JOB_INDEX); + + // user balance after = initial fund - 5 minutes worth tokens - notice period cost + const userBalanceExpected = SIGNER1_INITIAL_FUND.sub(usdcSpentExpected); + expect(await token.balanceOf(await user.getAddress())).to.be.within(userBalanceExpected.sub(JOB_RATE_1), userBalanceExpected.add(JOB_RATE_1)); - const providerPaymentThisSettle = amountPaidThisSettle; - const providerBalanceExpected = noticeCostPaidAtOpen.add(providerPaymentThisSettle); - expect(await token.balanceOf(await provider.getAddress())).to.be.closeTo(providerBalanceExpected, 2); + // provider balance after = 5 minutes worth tokens + notice period cost + const providerBalanceExpected = usdcSpentExpected; + expect(await token.balanceOf(await provider.getAddress())).to.be.within(providerBalanceExpected.sub(JOB_RATE_1), providerBalanceExpected.add(JOB_RATE_1)); - expect(await token.balanceOf(marketv1.address)).to.be.closeTo(jobBalanceExpected, 2); - expect(await creditToken.balanceOf(marketv1.address)).to.equal(0); + expect(await token.balanceOf(marketv1.address)).to.equal(0); + }); + }); + + describe("Scenario 4: Closing Job 2 minutes after notice period", function () { + it("should spend 12 minutes worth tokens", async () => { + const SEVEN_MINUTES = 60 * 7; + const usdcSpentExpected = calcAmountToPay(JOB_RATE_1, SEVEN_MINUTES).add(calcNoticePeriodCost(JOB_RATE_1)); + + await time.increaseTo(INITIAL_TIMESTAMP + NOTICE_PERIOD + SEVEN_MINUTES); + + await marketv1 + .connect(user) + .jobClose(INITIAL_JOB_INDEX); + + const userBalanceExpected = SIGNER1_INITIAL_FUND.sub(usdcSpentExpected); + expect(await token.balanceOf(await user.getAddress())).to.be.within(userBalanceExpected.sub(JOB_RATE_1), userBalanceExpected.add(JOB_RATE_1)); + + const providerBalanceExpected = usdcSpentExpected; + expect(await token.balanceOf(await provider.getAddress())).to.be.within(providerBalanceExpected.sub(JOB_RATE_1), providerBalanceExpected.add(JOB_RATE_1)); + + expect(await token.balanceOf(marketv1.address)).to.equal(0); }); }); }); - }); + + describe("Credit Only", function () { + const INITIAL_DEPOSIT_AMOUNT = usdc(50); + const NOTICE_PERIOD_COST = calcNoticePeriodCost(JOB_RATE_1); + + beforeEach(async () => { + // Deposit 50 Credit + await creditToken.connect(user).approve(marketv1.address, INITIAL_DEPOSIT_AMOUNT); + await marketv1.connect(user).jobOpen("some metadata", await provider.getAddress(), JOB_RATE_1, INITIAL_DEPOSIT_AMOUNT); + }); + + it("should close job and withdraw all credit", async () => { + expect(await marketv1.jobCreditBalance(INITIAL_JOB_INDEX)).to.equal(INITIAL_DEPOSIT_AMOUNT.sub(NOTICE_PERIOD_COST)); + const userCreditBalanceBefore = await creditToken.balanceOf(await user.getAddress()); + const userUSDCBalanceBefore = await token.balanceOf(await user.getAddress()); + + // Close job + await marketv1.connect(user).jobClose(INITIAL_JOB_INDEX); + + const userCreditBalanceExpected = userCreditBalanceBefore.add(INITIAL_DEPOSIT_AMOUNT.sub(NOTICE_PERIOD_COST)); + const userUSDCBalanceExpected = userUSDCBalanceBefore; + expect(await creditToken.balanceOf(await user.getAddress())).to.equal(userCreditBalanceExpected); + expect(await token.balanceOf(await user.getAddress())).to.equal(userUSDCBalanceExpected); + }); + }); + + describe("Both Credit and USDC", function () { + const INITIAL_DEPOSIT_AMOUNT = usdc(50); + const INITIAL_CREDIT_DEPOSIT_AMOUNT = usdc(40); + const NOTICE_PERIOD_COST = calcNoticePeriodCost(JOB_RATE_1); + + beforeEach(async () => { + // Deposit 40 Credit, 10 USDC + await creditToken.connect(user).approve(marketv1.address, INITIAL_CREDIT_DEPOSIT_AMOUNT); + await marketv1.connect(user).jobOpen("some metadata", await provider.getAddress(), JOB_RATE_1, INITIAL_DEPOSIT_AMOUNT); + }); + + it("should close job and withdraw all Credit and USDC", async () => { + const userCreditBalanceBefore = await creditToken.balanceOf(await user.getAddress()); + const userUSDCBalanceBefore = await token.balanceOf(await user.getAddress()); + + // Close job + await marketv1.connect(user).jobClose(INITIAL_JOB_INDEX); + + const userCreditBalanceExpected = userCreditBalanceBefore.add(INITIAL_CREDIT_DEPOSIT_AMOUNT.sub(NOTICE_PERIOD_COST)); + const userUSDCBalanceExpected = userUSDCBalanceBefore.add(INITIAL_DEPOSIT_AMOUNT.sub(INITIAL_CREDIT_DEPOSIT_AMOUNT)); + expect(await creditToken.balanceOf(await user.getAddress())).to.be.within(userCreditBalanceExpected.sub(JOB_RATE_1), userCreditBalanceExpected.add(JOB_RATE_1)); + expect(await token.balanceOf(await user.getAddress())).to.be.within(userUSDCBalanceExpected.sub(JOB_RATE_1), userUSDCBalanceExpected.add(JOB_RATE_1)); + }); + }); + }); + + describe("Metdata Update", function () { + const initialDeposit = usdc(50); + + takeSnapshotBeforeAndAfterEveryTest(async () => { }); + + beforeEach(async () => { + await token.connect(user).approve(marketv1.address, initialDeposit); + await marketv1 + .connect(user) + .jobOpen("some metadata", await provider.getAddress(), JOB_RATE_1, initialDeposit); + }); + + it("should update metadata", async () => { + await marketv1 + .connect(user) + .jobMetadataUpdate(INITIAL_JOB_INDEX, "some updated metadata"); + + const jobInfo2 = await marketv1.jobs(INITIAL_JOB_INDEX); + expect(jobInfo2.metadata).to.equal("some updated metadata"); + }); + + it("should revert when updating metadata of other jobs", async () => { + await expect(marketv1 + .connect(signers[3]) // neither owner nor provider + .jobMetadataUpdate(INITIAL_JOB_INDEX, "some updated metadata")).to.be.revertedWith("only job owner"); + }); + }); + + describe("Emergency Withdraw", function () { + const NUM_TOTAL_JOB = 5; + const INITIAL_DEPOSIT_AMOUNT = usdc(50); + let TOTAL_DEPOSIT_AMOUNT = BN.from(0); + let jobs: string[] = []; + let deposits: BN[] = []; + + beforeEach(async () => { + await marketv1.connect(admin).grantRole(await marketv1.EMERGENCY_WITHDRAW_ROLE(), await admin2.getAddress()); + + // open 5 jobs + for (let i = 0; i < NUM_TOTAL_JOB; i++) { + const EXTRA_DEPOSIT_AMOUNT = usdc(i * 10); + const DEPOSIT_AMOUNT = INITIAL_DEPOSIT_AMOUNT.add(EXTRA_DEPOSIT_AMOUNT); + + // list of jobs and deposits + jobs.push(await marketv1.jobIndex()); + deposits.push(DEPOSIT_AMOUNT); + // total credit deposit amount + TOTAL_DEPOSIT_AMOUNT = TOTAL_DEPOSIT_AMOUNT.add(DEPOSIT_AMOUNT); + + // open job only with credit + await creditToken.connect(user).approve(marketv1.address, DEPOSIT_AMOUNT); + await marketv1.connect(user).jobOpen("some metadata", await provider.getAddress(), JOB_RATE_1, DEPOSIT_AMOUNT); + } + }); + + it("should revert when withdrawing to address without EMERGENCY_WITHDRAW_ROLE", async () => { + await expect(marketv1 + .connect(admin) + .emergencyWithdrawCredit(await user.getAddress(), [INITIAL_JOB_INDEX])).to.be.revertedWith("only to emergency withdraw role"); + }); + + it("should revert when non-admin calls emergencyWithdrawCredit", async () => { + await expect(marketv1 + .connect(user) + .emergencyWithdrawCredit(await user.getAddress(), jobs)).to.be.revertedWith("only admin"); + }); + + it("should settle all jobs and withdraw all credit", async () => { + const totalSettledAmountExpected = calcNoticePeriodCost(JOB_RATE_1).mul(NUM_TOTAL_JOB); + + await marketv1.connect(admin).emergencyWithdrawCredit(await admin2.getAddress(), jobs, { gasLimit: 10000000 }); + + const CURRENT_TIMESTAMP = (await ethers.provider.getBlock('latest')).timestamp; + for (let i = 0; i < NUM_TOTAL_JOB; i++) { + // fetch job info + const jobInfo = await marketv1.jobs(jobs[i]); + + // should settle all jobs + expect(jobInfo.lastSettled).to.equal((CURRENT_TIMESTAMP).toString()); + + // job credit balance should be 0 + expect(await marketv1.jobCreditBalance(jobs[i])).to.equal(0); + } + + // withdrawal recipient + const withdrawalAmountExpected = TOTAL_DEPOSIT_AMOUNT.sub(totalSettledAmountExpected); + expect(await creditToken.balanceOf(await admin2.getAddress())).to.be.within(withdrawalAmountExpected.sub(JOB_RATE_1), withdrawalAmountExpected.add(JOB_RATE_1)); + + // Provider + expect(await token.balanceOf(await provider.getAddress())).to.be.within(totalSettledAmountExpected.sub(JOB_RATE_1), totalSettledAmountExpected.add(JOB_RATE_1)); + + // MarketV1 + expect(await creditToken.balanceOf(marketv1.address)).to.equal(0); + }); + }) }); \ No newline at end of file