From 1fff2f7295121e02c7d2a43b47c3a220eb444bee Mon Sep 17 00:00:00 2001 From: Eric Mastro Date: Wed, 21 Sep 2022 19:38:42 +1000 Subject: [PATCH] [marketplace] add isFinished Add function `isFinished` that checks whethere the state has been set to `RequestState.Finished` (which is not being set yet) or if the contract was started (`RequestState.Started`) and the contract duration has lapsed. To facilitate `isFinished`, the contract start time (`startedAt`) was added to calculate the contract end time. --- contracts/Marketplace.sol | 22 +++++++++++++++++++--- contracts/Proofs.sol | 2 +- test/Marketplace.test.js | 25 ++++++++++++++++++++----- test/Storage.test.js | 37 ++++++++++++++++++++++++++++++++++--- 4 files changed, 74 insertions(+), 12 deletions(-) diff --git a/contracts/Marketplace.sol b/contracts/Marketplace.sol index 8d65687..e7ec37b 100644 --- a/contracts/Marketplace.sol +++ b/contracts/Marketplace.sol @@ -104,6 +104,7 @@ contract Marketplace is Collateral, Proofs { emit SlotFilled(requestId, slotIndex, slotId); if (context.slotsFilled == request.ask.slots) { context.state = RequestState.Started; + context.startedAt = block.timestamp; _extendLockExpiry(requestId, block.timestamp + request.ask.duration); emit RequestFulfilled(requestId); } @@ -146,10 +147,9 @@ contract Marketplace is Collateral, Proofs { public marketplaceInvariant { + require(_isFinished(requestId), "Contract not ended"); bytes32 slotId = keccak256(abi.encode(requestId, slotIndex)); - require(block.timestamp > proofEnd(slotId), "Contract not ended"); - Slot storage slot = slots[slotId]; - require(slot.host != address(0), "Slot empty"); + Slot storage slot = _slot(slotId); require(!slot.hostPaid, "Already paid"); uint256 amount = pricePerSlot(requests[requestId]); funds.sent += amount; @@ -196,6 +196,21 @@ contract Marketplace is Collateral, Proofs { ); } + /// @notice Return true if the request state is RequestState.Finished or if the request duration has elapsed and the request was started. + /// @dev Handles the case when a request may have been finished, but the state has not yet been updated by a transaction. + /// @param requestId the id of the request + /// @return true if request is finished + function _isFinished(bytes32 requestId) internal view returns (bool) { + RequestContext memory context = requestContexts[requestId]; + Request memory request = _request(requestId); + return + context.state == RequestState.Finished || + ( + context.state == RequestState.Started && + block.timestamp > context.startedAt + request.ask.duration + ); + } + /// @notice Return id of request that slot belongs to /// @dev Returns requestId that is mapped to the slotId /// @param slotId id of the slot @@ -342,6 +357,7 @@ contract Marketplace is Collateral, Proofs { struct RequestContext { uint256 slotsFilled; RequestState state; + uint256 startedAt; } struct Slot { diff --git a/contracts/Proofs.sol b/contracts/Proofs.sol index 822c1d2..3b5f1cd 100644 --- a/contracts/Proofs.sol +++ b/contracts/Proofs.sol @@ -34,7 +34,7 @@ contract Proofs { return timeout; } - function _end(bytes32 id) internal view returns (uint256) { + function _end(bytes32 id) internal view returns (uint256) { return ends[id]; } diff --git a/test/Marketplace.test.js b/test/Marketplace.test.js index 616e74f..dbacd61 100644 --- a/test/Marketplace.test.js +++ b/test/Marketplace.test.js @@ -324,7 +324,12 @@ describe("Marketplace", function () { } it("pays the host", async function () { - await marketplace.fillSlot(slot.request, slot.index, proof) + await waitUntilAllSlotsFilled( + marketplace, + request.ask.slots, + slot.request, + proof + ) await waitUntilEnd() const startBalance = await token.balanceOf(host.address) await marketplace.payoutSlot(slot.request, slot.index) @@ -332,10 +337,10 @@ describe("Marketplace", function () { expect(endBalance - startBalance).to.equal(pricePerSlot(request)) }) - it("is only allowed when the slot is filled", async function () { + it("is only allowed when the contract is finished", async function () { await expect( marketplace.payoutSlot(slot.request, slot.index) - ).to.be.revertedWith("Slot empty") + ).to.be.revertedWith("Contract not ended") }) it("is only allowed when the contract has ended", async function () { @@ -346,7 +351,12 @@ describe("Marketplace", function () { }) it("can only be done once", async function () { - await marketplace.fillSlot(slot.request, slot.index, proof) + await waitUntilAllSlotsFilled( + marketplace, + request.ask.slots, + slot.request, + proof + ) await waitUntilEnd() await marketplace.payoutSlot(slot.request, slot.index) await expect( @@ -355,7 +365,12 @@ describe("Marketplace", function () { }) it("cannot be filled again", async function () { - await marketplace.fillSlot(slot.request, slot.index, proof) + await waitUntilAllSlotsFilled( + marketplace, + request.ask.slots, + slot.request, + proof + ) await waitUntilEnd() await marketplace.payoutSlot(slot.request, slot.index) await expect(marketplace.fillSlot(slot.request, slot.index, proof)).to.be diff --git a/test/Storage.test.js b/test/Storage.test.js index f6e2995..59476ea 100644 --- a/test/Storage.test.js +++ b/test/Storage.test.js @@ -193,8 +193,7 @@ describe("Storage", function () { await advanceTimeTo(request.expiry + 1) await expect(await storage.willProofBeRequired(id)).to.be.false }) - - + it("does not require proofs once cancelled", async function () { const id = slotId(slot) @@ -286,8 +285,40 @@ describe("Storage", function () { await markProofAsMissing(id, onMarkAsMissing) await expect(storage.getSlot(id)).to.be.revertedWith("Slot empty") }) - }) + }) + describe("contract state", function () { + it("isCancelled is true once request is cancelled", async function () { + await expect(await storage.isCancelled(slot.request)).to.equal(false) + await waitUntilExpired(request.expiry) + await expect(await storage.isCancelled(slot.request)).to.equal(true) + }) + it("isSlotCancelled fails when slot is empty", async function () { + await expect(storage.isSlotCancelled(slotId(slot))).to.be.revertedWith( + "Slot empty" + ) + }) + + it("isSlotCancelled is true once request is cancelled", async function () { + await storage.fillSlot(slot.request, slot.index, proof) + await waitUntilExpired(request.expiry) + await expect(await storage.isSlotCancelled(slotId(slot))).to.equal(true) + }) + + it("isFinished is true once started and contract duration lapses", async function () { + await expect(await storage.isFinished(slot.request)).to.be.false + // fill all slots, should change state to RequestState.Started + await waitUntilAllSlotsFilled( + storage, + request.ask.slots, + slot.request, + proof + ) + await expect(await storage.isFinished(slot.request)).to.be.false + advanceTime(request.ask.duration + 1) + await expect(await storage.isFinished(slot.request)).to.be.true + }) + }) }) // TODO: implement checking of actual proofs of storage, instead of dummy bool