diff --git a/contracts/AccountLocks.sol b/contracts/AccountLocks.sol index 65fecab..1d0ba36 100644 --- a/contracts/AccountLocks.sol +++ b/contracts/AccountLocks.sol @@ -46,12 +46,12 @@ contract AccountLocks { /// NOTE: We do not need to check that msg.sender is the lock.owner because /// this function is internal, and is only called after all checks have been /// performed in Marketplace.fillSlot. - function _extendLockExpiry(bytes32 lockId, uint256 duration) internal { + function _extendLockExpiryTo(bytes32 lockId, uint256 expiry) internal { Lock storage lock = locks[lockId]; require(lock.owner != address(0), "Lock does not exist"); // require(lock.owner == msg.sender, "Only lock creator can extend expiry"); require(lock.expiry >= block.timestamp, "Lock already expired"); - lock.expiry += duration; + lock.expiry = expiry; } /// Unlocks an account. This will fail if there are any active locks attached diff --git a/contracts/Marketplace.sol b/contracts/Marketplace.sol index e7ec37b..3ab0ae0 100644 --- a/contracts/Marketplace.sol +++ b/contracts/Marketplace.sol @@ -47,38 +47,6 @@ contract Marketplace is Collateral, Proofs { emit StorageRequested(id, request.ask); } - function _freeSlot( - bytes32 slotId - ) internal marketplaceInvariant { - Slot storage slot = _slot(slotId); - bytes32 requestId = slot.requestId; - RequestContext storage context = requestContexts[requestId]; - require(context.state == RequestState.Started, "Invalid state"); - - // TODO: burn host's slot collateral except for repair costs + mark proof - // missing reward - // Slot collateral is not yet implemented as the design decision was - // not finalised. - - _unexpectProofs(slotId); - - slot.host = address(0); - slot.requestId = 0; - context.slotsFilled -= 1; - emit SlotFreed(requestId, slotId); - - Request memory request = _request(requestId); - uint256 slotsLost = request.ask.slots - context.slotsFilled; - if (slotsLost > request.ask.maxSlotLoss) { - context.state = RequestState.Failed; - emit RequestFailed(requestId); - - // TODO: burn all remaining slot collateral (note: slot collateral not - // yet implemented) - // TODO: send client remaining funds - } - } - function fillSlot( bytes32 requestId, uint256 slotIndex, @@ -94,18 +62,20 @@ contract Marketplace is Collateral, Proofs { require(balanceOf(msg.sender) >= collateral, "Insufficient collateral"); _lock(msg.sender, requestId); - _expectProofs(slotId, request.ask.proofProbability, request.ask.duration); + _expectProofs(slotId, requestId, request.ask.proofProbability, request.ask.duration); _submitProof(slotId, proof); slot.host = msg.sender; slot.requestId = requestId; - RequestContext storage context = requestContexts[requestId]; + RequestContext storage context = _context(requestId); context.slotsFilled += 1; 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); + context.endsAt = block.timestamp + request.ask.duration; + _extendLockExpiryTo(requestId, context.endsAt); + _extendProofEndTo(slotId, context.endsAt); emit RequestFulfilled(requestId); } } @@ -148,6 +118,9 @@ contract Marketplace is Collateral, Proofs { marketplaceInvariant { require(_isFinished(requestId), "Contract not ended"); + RequestContext storage context = _context(requestId); + context.state = RequestState.Finished; + bytes32 slotId = keccak256(abi.encode(requestId, slotIndex)); Slot storage slot = _slot(slotId); require(!slot.hostPaid, "Already paid"); @@ -165,7 +138,7 @@ contract Marketplace is Collateral, Proofs { Request storage request = requests[requestId]; require(block.timestamp > request.expiry, "Request not yet timed out"); require(request.client == msg.sender, "Invalid client address"); - RequestContext storage context = requestContexts[requestId]; + RequestContext storage context = _context(requestId); require(context.state == RequestState.New, "Invalid state"); // Update request state to Cancelled. Handle in the withdraw transaction @@ -201,13 +174,12 @@ contract Marketplace is Collateral, Proofs { /// @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); + RequestContext memory context = _context(requestId); return context.state == RequestState.Finished || ( context.state == RequestState.Started && - block.timestamp > context.startedAt + request.ask.duration + block.timestamp > context.endsAt ); } @@ -234,8 +206,8 @@ contract Marketplace is Collateral, Proofs { return slots[slotId].host; } - function _request(bytes32 id) internal view returns (Request storage) { - Request storage request = requests[id]; + function _request(bytes32 requestId) internal view returns (Request storage) { + Request storage request = requests[requestId]; require(request.client != address(0), "Unknown request"); return request; } @@ -259,6 +231,7 @@ contract Marketplace is Collateral, Proofs { } function proofEnd(bytes32 slotId) public view returns (uint256) { + Slot memory slot = _slot(slotId); uint256 end = _end(slotId); if (!_slotAcceptsProofs(slotId)) { return end < block.timestamp ? end : block.timestamp - 1; @@ -290,6 +263,8 @@ contract Marketplace is Collateral, Proofs { // TODO: add check for _isFinished if (_isCancelled(requestId)) { return RequestState.Cancelled; + else if (_isFinished(requestId) { + return RequestState.Finished; } else { RequestContext storage context = _context(requestId); return context.state; @@ -358,6 +333,7 @@ contract Marketplace is Collateral, Proofs { uint256 slotsFilled; RequestState state; uint256 startedAt; + uint256 endsAt; } struct Slot { @@ -385,6 +361,15 @@ contract Marketplace is Collateral, Proofs { assert(funds.received == funds.balance + funds.sent); } + function acceptsProofs(bytes32 requestId) private view { + RequestState s = state(requestId); + require(s == RequestState.New || s == RequestState.Started, "Invalid state"); + // must test these states separately as they handle cases where the state hasn't + // yet been updated by a transaction + require(!_isCancelled(requestId), "Request cancelled"); + require(!_isFinished(requestId), "Request finished"); + } + /// @notice Modifier that requires the request state to be that which is accepting proof submissions from hosts occupying slots. /// @dev Request state must be new or started, and must not be cancelled, finished, or failed. /// @param slotId id of the slot, that is mapped to a request, for which to obtain state info diff --git a/contracts/Proofs.sol b/contracts/Proofs.sol index 3b5f1cd..3ffa3e1 100644 --- a/contracts/Proofs.sol +++ b/contracts/Proofs.sol @@ -20,6 +20,7 @@ contract Proofs { mapping(bytes32 => bool) private ids; mapping(bytes32 => uint256) private starts; mapping(bytes32 => uint256) private ends; + mapping(bytes32 => bytes32) private idEnds; mapping(bytes32 => uint256) private probabilities; mapping(bytes32 => uint256) private markers; mapping(bytes32 => uint256) private missed; @@ -34,8 +35,21 @@ contract Proofs { return timeout; } - function _end(bytes32 id) internal view returns (uint256) { - return ends[id]; + function _end(bytes32 endId) internal view returns (uint256) { + uint256 end = ends[endId]; + require(end > 0, "Proof ending doesn't exist"); + return ends[endId]; + } + + function _endId(bytes32 id) internal view returns (bytes32) { + bytes32 endId = idEnds[id]; + require(endId > 0, "endId for given id doesn't exist"); + return endId; + } + + function _endFromId(bytes32 id) internal view returns (uint256) { + bytes32 endId = _endId(id); + return _end(endId); } function _missed(bytes32 id) internal view returns (uint256) { @@ -50,17 +64,25 @@ contract Proofs { return periodOf(block.timestamp); } + /// @notice Informs the contract that proofs should be expected for id + /// @dev Requires that the id is not already in use + /// @param id identifies the proof expectation, typically a slot id + /// @param endId Identifies the id of the proof expectation ending. Typically a request id. Different from id because the proof ending is shared amongst many ids. + /// @param probability The probability that a proof should be expected + /// @param duration Duration, from now, for which proofs should be expected function _expectProofs( - bytes32 id, + bytes32 id, // typically slot id + bytes32 endId, // typically request id, used so that the ending is global for all slots uint256 probability, uint256 duration ) internal { require(!ids[id], "Proof id already in use"); ids[id] = true; starts[id] = block.timestamp; - ends[id] = block.timestamp + duration; + ends[endId] = block.timestamp + duration; probabilities[id] = probability; markers[id] = uint256(blockhash(block.number - 1)) % period; + idEnds[id] = endId; } function _unexpectProofs( @@ -112,7 +134,8 @@ contract Proofs { if (proofPeriod <= periodOf(starts[id])) { return (false, 0); } - if (proofPeriod >= periodOf(ends[id])) { + uint256 end = _endFromId(id); + if (proofPeriod >= periodOf(end)) { return (false, 0); } pointer = _getPointer(id, proofPeriod); @@ -162,5 +185,19 @@ contract Proofs { missed[id] += 1; } + /// @notice Extends the proof end time + /// @dev The id must have a mapping to an end id, the end must exist, and the end must not have elapsed yet + /// @param id the id of the proofs to extend. Typically a slot id, the id is mapped to an endId. + /// @param ending the new end time (in seconds) + function _extendProofEndTo(bytes32 id, uint256 ending) internal { + bytes32 endId = _endId(id); + uint256 end = ends[endId]; + // TODO: create type aliases for id and endId so that _end() can return + // EndId storage and we don't need to replicate the below require here + require (end > 0, "Proof ending doesn't exist"); + require (block.timestamp <= end, "Proof already ended"); + ends[endId] = ending; + } + event ProofSubmitted(bytes32 id, bytes proof); } diff --git a/contracts/Storage.sol b/contracts/Storage.sol index 7699c45..c0d0cdc 100644 --- a/contracts/Storage.sol +++ b/contracts/Storage.sol @@ -82,7 +82,6 @@ contract Storage is Collateral, Marketplace { function markProofAsMissing(bytes32 slotId, uint256 period) public - slotMustAcceptProofs(slotId) { _markProofAsMissing(slotId, period); address host = _host(slotId); diff --git a/contracts/TestAccountLocks.sol b/contracts/TestAccountLocks.sol index 48f2aa8..e16340b 100644 --- a/contracts/TestAccountLocks.sol +++ b/contracts/TestAccountLocks.sol @@ -21,7 +21,7 @@ contract TestAccountLocks is AccountLocks { _unlockAccount(); } - function extendLockExpiry(bytes32 lockId, uint256 expiry) public { - _extendLockExpiry(lockId, expiry); + function extendLockExpiryTo(bytes32 lockId, uint256 expiry) public { + _extendLockExpiryTo(lockId, expiry); } } diff --git a/contracts/TestMarketplace.sol b/contracts/TestMarketplace.sol index e15be96..ad88546 100644 --- a/contracts/TestMarketplace.sol +++ b/contracts/TestMarketplace.sol @@ -33,4 +33,13 @@ contract TestMarketplace is Marketplace { function slot(bytes32 slotId) public view returns (Slot memory) { return _slot(slotId); } + + function testAcceptsProofs(bytes32 slotId) + public + view + slotAcceptsProofs(slotId) + // solhint-disable-next-line no-empty-blocks + { + + } } diff --git a/contracts/TestProofs.sol b/contracts/TestProofs.sol index 9cdb8fb..aa6a37f 100644 --- a/contracts/TestProofs.sol +++ b/contracts/TestProofs.sol @@ -34,10 +34,11 @@ contract TestProofs is Proofs { function expectProofs( bytes32 id, + bytes32 endId, uint256 _probability, uint256 _duration ) public { - _expectProofs(id, _probability, _duration); + _expectProofs(id, endId, _probability, _duration); } function unexpectProofs(bytes32 id) public { @@ -67,4 +68,8 @@ contract TestProofs is Proofs { function markProofAsMissing(bytes32 id, uint256 _period) public { _markProofAsMissing(id, _period); } + + function extendProofEndTo(bytes32 id, uint256 ending) public { + _extendProofEndTo(id, ending); + } } diff --git a/test/AccountLocks.test.js b/test/AccountLocks.test.js index 1fbee6a..689adac 100644 --- a/test/AccountLocks.test.js +++ b/test/AccountLocks.test.js @@ -1,7 +1,7 @@ const { ethers } = require("hardhat") const { expect } = require("chai") const { hexlify, randomBytes, toHexString } = ethers.utils -const { advanceTimeTo, snapshot, revert } = require("./evm") +const { advanceTimeTo, snapshot, revert, advanceTime } = require("./evm") const { exampleLock } = require("./examples") const { now, hours } = require("./time") const { waitUntilExpired } = require("./marketplace") @@ -188,7 +188,7 @@ describe("Account Locks", function () { it("fails when lock id doesn't exist", async function () { let other = exampleLock() await expect( - locks.extendLockExpiry(other.id, hours(1)) + locks.extendLockExpiryTo(other.id, now() + hours(1)) ).to.be.revertedWith("Lock does not exist") }) @@ -200,7 +200,16 @@ describe("Account Locks", function () { }) it("successfully updates lock expiry", async function () { - await expect(locks.extendLockExpiry(id, hours(1))).not.to.be.reverted + await expect(locks.extendLockExpiryTo(id, now() + hours(1))).not.to.be + .reverted + }) + + it("unlocks account after expiry", async function () { + await expect(locks.extendLockExpiryTo(id, now() + hours(1))).not.to.be + .reverted + await expect(locks.unlockAccount()).to.be.revertedWith("Account locked") + advanceTime(hours(1)) + await expect(locks.unlockAccount()).not.to.be.reverted }) }) }) diff --git a/test/Marketplace.test.js b/test/Marketplace.test.js index dbacd61..c574e32 100644 --- a/test/Marketplace.test.js +++ b/test/Marketplace.test.js @@ -2,16 +2,21 @@ const { ethers } = require("hardhat") const { hexlify, randomBytes } = ethers.utils const { expect } = require("chai") const { exampleRequest } = require("./examples") -const { now, hours } = require("./time") +const { now, hours, minutes } = require("./time") const { requestId, slotId, askToArray } = require("./ids") -const { waitUntilExpired, waitUntilAllSlotsFilled, RequestState } = require("./marketplace") +const { waitUntilExpired, waitUntilAllSlotsFilled } = require("./marketplace") + waitUntilCancelled, + waitUntilStarted, + waitUntilFinished, + waitUntilFailed, +} = require("./marketplace") const { price, pricePerSlot } = require("./price") const { snapshot, revert, ensureMinimumBlockHeight, - advanceTimeTo, - currentTime, + advanceTime, + currentTime } = require("./evm") describe("Marketplace", function () { @@ -115,8 +120,6 @@ describe("Marketplace", function () { switchAccount(host) await token.approve(marketplace.address, collateral) await marketplace.deposit(collateral) - - // await marketplace.fillSlot(slot.request, slot.index, proof) }) it("fails to free slot when slot not filled", async function () { @@ -132,8 +135,21 @@ describe("Marketplace", function () { await expect(marketplace.freeSlot(id)).to.be.revertedWith("Invalid state") }) + it("fails to free slot when finished", async function () { + await waitUntilStarted( + marketplace, + request.ask.slots, + slot.request, + proof + ) + await waitUntilFinished(marketplace, slotId(slot)) + await expect(marketplace.freeSlot(id)).to.be.revertedWith( + "Request finished" + ) + }) + it("successfully frees slot", async function () { - await waitUntilAllSlotsFilled( + await waitUntilStarted( marketplace, request.ask.slots, slot.request, @@ -143,7 +159,7 @@ describe("Marketplace", function () { }) it("emits event once slot is freed", async function () { - await waitUntilAllSlotsFilled( + await waitUntilStarted( marketplace, request.ask.slots, slot.request, @@ -155,7 +171,7 @@ describe("Marketplace", function () { }) it("cannot get slot once freed", async function () { - await waitUntilAllSlotsFilled( + await waitUntilStarted( marketplace, request.ask.slots, slot.request, @@ -234,6 +250,32 @@ describe("Marketplace", function () { ).to.be.revertedWith("Request not accepting proofs") }) + it("is rejected when request is finished", async function () { + await waitUntilStarted( + marketplace, + request.ask.slots, + slot.request, + proof + ) + await waitUntilFinished(marketplace, slotId(slot)) + await expect( + marketplace.fillSlot(slot.request, slot.index, proof) + ).to.be.revertedWith("Request finished") + }) + + it("is rejected when request is failed", async function () { + await waitUntilStarted( + marketplace, + request.ask.slots, + slot.request, + proof + ) + await waitUntilFailed(marketplace, slot, request.ask.maxSlotLoss) + await expect( + marketplace.fillSlot(slot.request, slot.index, proof) + ).to.be.revertedWith("Invalid state") + }) + it("is rejected when slot index not in range", async function () { const invalid = request.ask.slots await expect( @@ -250,6 +292,20 @@ describe("Marketplace", function () { marketplace.fillSlot(slot.request, lastSlot, proof) ).to.be.revertedWith("Slot already filled") }) + it("shares proof end time for all slots in request", async function () { + const lastSlot = request.ask.slots - 1 + for (let i = 0; i < lastSlot; i++) { + await marketplace.fillSlot(slot.request, i, proof) + } + advanceTime(minutes(10)) + await marketplace.fillSlot(slot.request, lastSlot, proof) + let slot0 = { ...slot, index: 0 } + let end = await marketplace.proofEnd(slotId(slot0)) + for (let i = 1; i <= lastSlot; i++) { + let sloti = { ...slot, index: i } + await expect((await marketplace.proofEnd(slotId(sloti))) === end) + } + }) }) describe("freeing a slot", function () { @@ -318,19 +374,14 @@ describe("Marketplace", function () { await marketplace.deposit(collateral) }) - async function waitUntilEnd() { - const end = (await marketplace.proofEnd(slotId(slot))).toNumber() - await advanceTimeTo(end) - } - it("pays the host", async function () { - await waitUntilAllSlotsFilled( + await waitUntilStarted( marketplace, request.ask.slots, slot.request, proof ) - await waitUntilEnd() + await waitUntilFinished(marketplace, slotId(slot)) const startBalance = await token.balanceOf(host.address) await marketplace.payoutSlot(slot.request, slot.index) const endBalance = await token.balanceOf(host.address) @@ -351,13 +402,13 @@ describe("Marketplace", function () { }) it("can only be done once", async function () { - await waitUntilAllSlotsFilled( + await waitUntilStarted( marketplace, request.ask.slots, slot.request, proof ) - await waitUntilEnd() + await waitUntilFinished(marketplace, slotId(slot)) await marketplace.payoutSlot(slot.request, slot.index) await expect( marketplace.payoutSlot(slot.request, slot.index) @@ -365,13 +416,13 @@ describe("Marketplace", function () { }) it("cannot be filled again", async function () { - await waitUntilAllSlotsFilled( + await waitUntilStarted( marketplace, request.ask.slots, slot.request, proof ) - await waitUntilEnd() + await waitUntilFinished(marketplace, slotId(slot)) await marketplace.payoutSlot(slot.request, slot.index) await expect(marketplace.fillSlot(slot.request, slot.index, proof)).to.be .reverted @@ -403,8 +454,15 @@ describe("Marketplace", function () { await marketplace.fillSlot(slot.request, i, proof) } await expect(await marketplace.state(slot.request)).to.equal( - RequestState.Started - ) + }) + it("fails when all slots are already filled", async function () { + const lastSlot = request.ask.slots - 1 + for (let i = 0; i <= lastSlot; i++) { + await marketplace.fillSlot(slot.request, i, proof) + } + await expect( + marketplace.fillSlot(slot.request, lastSlot, proof) + ).to.be.revertedWith("Slot already filled") }) }) @@ -426,7 +484,7 @@ describe("Marketplace", function () { }) it("rejects withdraw when wrong account used", async function () { - await waitUntilExpired(request.expiry) + await waitUntilCancelled(request.expiry) await expect(marketplace.withdrawFunds(slot.request)).to.be.revertedWith( "Invalid client address" ) @@ -438,7 +496,7 @@ describe("Marketplace", function () { for (let i = 0; i <= lastSlot; i++) { await marketplace.fillSlot(slot.request, i, proof) } - await waitUntilExpired(request.expiry) + await waitUntilCancelled(request.expiry) switchAccount(client) await expect(marketplace.withdrawFunds(slot.request)).to.be.revertedWith( "Invalid state" @@ -446,7 +504,7 @@ describe("Marketplace", function () { }) it("emits event once request is cancelled", async function () { - await waitUntilExpired(request.expiry) + await waitUntilCancelled(request.expiry) switchAccount(client) await expect(marketplace.withdrawFunds(slot.request)) .to.emit(marketplace, "RequestCancelled") @@ -454,7 +512,7 @@ describe("Marketplace", function () { }) it("withdraws to the client", async function () { - await waitUntilExpired(request.expiry) + await waitUntilCancelled(request.expiry) switchAccount(client) const startBalance = await token.balanceOf(client.address) await marketplace.withdrawFunds(slot.request) @@ -473,11 +531,14 @@ describe("Marketplace", function () { await marketplace.deposit(collateral) }) - it("changes state to Cancelled when client withdraws funds", async function () { + it("state is Cancelled when client withdraws funds", async function () { await expect(await marketplace.state(slot.request)).to.equal( RequestState.New ) - await waitUntilExpired(request.expiry) + }) + + it("state is Cancelled when client withdraws funds", async function () { + await waitUntilCancelled(request.expiry) switchAccount(client) await marketplace.withdrawFunds(slot.request) await expect(await marketplace.state(slot.request)).to.equal( @@ -486,10 +547,7 @@ describe("Marketplace", function () { }) it("changes state to Started once all slots are filled", async function () { - await expect(await marketplace.state(slot.request)).to.equal( - RequestState.New - ) - await waitUntilAllSlotsFilled( + await waitUntilStarted( marketplace, request.ask.slots, slot.request, @@ -500,23 +558,32 @@ describe("Marketplace", function () { ) }) - // fill all slots, should change state to RequestState.Started - await waitUntilAllSlotsFilled( + it("state is Failed once too many slots are freed", async function () { + await waitUntilStarted( marketplace, request.ask.slots, slot.request, proof ) - for (let i = 0; i <= request.ask.maxSlotLoss; i++) { - slot.index = i - let id = slotId(slot) - await marketplace.freeSlot(id) - } + await waitUntilFailed(marketplace, slot, request.ask.maxSlotLoss) await expect(await marketplace.state(slot.request)).to.equal( RequestState.Failed ) }) + it("state is Finished once slot is paid out", async function () { + await waitUntilStarted( + marketplace, + request.ask.slots, + slot.request, + proof + ) + await waitUntilFinished(marketplace, slotId(slot)) + await marketplace.payoutSlot(slot.request, slot.index) + await expect(await marketplace.state(slot.request)).to.equal( + RequestState.Finished + ) + }) it("does not change state to Failed if too many slots freed but contract not started", async function () { for (let i = 0; i <= request.ask.maxSlotLoss; i++) { await marketplace.fillSlot(slot.request, i, proof) @@ -568,4 +635,79 @@ describe("Marketplace", function () { ) }) }) + describe("modifiers", function () { + beforeEach(async function () { + switchAccount(client) + await token.approve(marketplace.address, price(request)) + await marketplace.requestStorage(request) + switchAccount(host) + await token.approve(marketplace.address, collateral) + await marketplace.deposit(collateral) + }) + + describe("accepting proofs", function () { + it("fails when request Cancelled (isCancelled is true)", async function () { + await marketplace.fillSlot(slot.request, slot.index, proof) + await waitUntilCancelled(request.expiry) + await expect( + marketplace.testAcceptsProofs(slotId(slot)) + ).to.be.revertedWith("Request cancelled") + }) + + it("fails when request Cancelled (state set to Cancelled)", async function () { + await marketplace.fillSlot(slot.request, slot.index, proof) + await waitUntilCancelled(request.expiry) + switchAccount(client) + await marketplace.withdrawFunds(slot.request) + await expect( + marketplace.testAcceptsProofs(slotId(slot)) + ).to.be.revertedWith("Invalid state") + }) + + it("fails when request Finished (isFinished is true)", async function () { + await waitUntilStarted( + marketplace, + request.ask.slots, + slot.request, + proof + ) + await waitUntilFinished(marketplace, slotId(slot)) + await expect( + marketplace.testAcceptsProofs(slotId(slot)) + ).to.be.revertedWith("Request finished") + }) + + it("fails when request Finished (state set to Finished)", async function () { + await waitUntilStarted( + marketplace, + request.ask.slots, + slot.request, + proof + ) + await waitUntilFinished(marketplace, slotId(slot)) + await marketplace.payoutSlot(slot.request, slot.index) + await expect( + marketplace.testAcceptsProofs(slotId(slot)) + ).to.be.revertedWith("Invalid state") + }) + + it("fails when request Failed", async function () { + await waitUntilStarted( + marketplace, + request.ask.slots, + slot.request, + proof + ) + for (let i = 0; i <= request.ask.maxSlotLoss; i++) { + slot.index = i + let id = slotId(slot) + await marketplace.freeSlot(id) + } + await expect( + marketplace.testAcceptsProofs(slotId(slot)) + ).to.be.revertedWith("Slot empty") + }) + }) + + }) }) diff --git a/test/Proofs.test.js b/test/Proofs.test.js index 71cc96e..414fd9a 100644 --- a/test/Proofs.test.js +++ b/test/Proofs.test.js @@ -10,10 +10,11 @@ const { advanceTime, advanceTimeTo, } = require("./evm") -const { periodic } = require("./time") +const { periodic, hours, now, minutes } = require("./time") describe("Proofs", function () { const id = hexlify(randomBytes(32)) + const endId = hexlify(randomBytes(32)) const period = 30 * 60 const timeout = 5 const downtime = 64 @@ -35,20 +36,20 @@ describe("Proofs", function () { }) it("calculates an end time based on duration", async function () { - await proofs.expectProofs(id, probability, duration) + await proofs.expectProofs(id, endId, probability, duration) let end = (await currentTime()) + duration - expect((await proofs.end(id)).toNumber()).to.be.closeTo(end, 1) + expect((await proofs.end(endId)).toNumber()).to.be.closeTo(end, 1) }) it("does not allow ids to be reused", async function () { - await proofs.expectProofs(id, probability, duration) + await proofs.expectProofs(id, endId, probability, duration) await expect( - proofs.expectProofs(id, probability, duration) + proofs.expectProofs(id, endId, probability, duration) ).to.be.revertedWith("Proof id already in use") }) it("requires proofs with an agreed upon probability", async function () { - await proofs.expectProofs(id, probability, duration) + await proofs.expectProofs(id, endId, probability, duration) let amount = 0 for (let i = 0; i < 100; i++) { if (await proofs.isProofRequired(id)) { @@ -63,7 +64,7 @@ describe("Proofs", function () { it("requires no proofs in the start period", async function () { const startPeriod = Math.floor((await currentTime()) / period) const probability = 1 - await proofs.expectProofs(id, probability, duration) + await proofs.expectProofs(id, endId, probability, duration) while (Math.floor((await currentTime()) / period) == startPeriod) { expect(await proofs.isProofRequired(id)).to.be.false await advanceTime(Math.floor(period / 10)) @@ -72,14 +73,14 @@ describe("Proofs", function () { it("requires no proofs in the end period", async function () { const probability = 1 - await proofs.expectProofs(id, probability, duration) + await proofs.expectProofs(id, endId, probability, duration) await advanceTime(duration) expect(await proofs.isProofRequired(id)).to.be.false }) it("requires no proofs after the end time", async function () { const probability = 1 - await proofs.expectProofs(id, probability, duration) + await proofs.expectProofs(id, endId, probability, duration) await advanceTime(duration + timeout) expect(await proofs.isProofRequired(id)).to.be.false }) @@ -89,7 +90,7 @@ describe("Proofs", function () { let id2 = hexlify(randomBytes(32)) let id3 = hexlify(randomBytes(32)) for (let id of [id1, id2, id3]) { - await proofs.expectProofs(id, probability, duration) + await proofs.expectProofs(id, endId, probability, duration) } let req1, req2, req3 while (req1 === req2 && req2 === req3) { @@ -118,7 +119,7 @@ describe("Proofs", function () { } beforeEach(async function () { - await proofs.expectProofs(id, probability, duration) + await proofs.expectProofs(id, endId, probability, duration) await advanceTimeTo(periodEnd(periodOf(await currentTime()))) await waitUntilProofWillBeRequired() }) @@ -151,7 +152,7 @@ describe("Proofs", function () { const proof = hexlify(randomBytes(42)) beforeEach(async function () { - await proofs.expectProofs(id, probability, duration) + await proofs.expectProofs(id, endId, probability, duration) }) async function waitUntilProofIsRequired(id) { @@ -270,4 +271,87 @@ describe("Proofs", function () { await expect(await proofs.isProofRequired(id)).to.be.false }) }) + + describe("extend proof end", function () { + const proof = hexlify(randomBytes(42)) + + beforeEach(async function () { + await proofs.expectProofs(id, endId, probability, duration) + }) + + async function waitUntilProofIsRequired(id) { + await advanceTimeTo(periodEnd(periodOf(await currentTime()))) + while ( + !( + (await proofs.isProofRequired(id)) && + (await proofs.getPointer(id)) < 250 + ) + ) { + await advanceTime(period) + } + } + + async function isProofRequiredBefore(id, ending) { + let start = periodOf(await currentTime()) + let end = periodOf(ending) + let periods = end - start + await advanceTimeTo(periodEnd(periodOf(await currentTime()))) + for (let i = 0; i < periods; i++) { + if (await proofs.isProofRequired(id)) { + return true + } + await advanceTime(period) + } + return false + } + + it("can't extend if proof doesn't exist", async function () { + let ending = (await currentTime()) + duration + const otherId = hexlify(randomBytes(32)) + await expect( + proofs.extendProofEndTo(otherId, ending + 1) + ).to.be.revertedWith("endId for given id doesn't exist") + }) + + it("can't extend already lapsed proof ending", async function () { + let ending = (await currentTime()) + duration + await waitUntilProofIsRequired(id) + await advanceTimeTo(ending + 1) + await expect(proofs.extendProofEndTo(id, ending + 1)).to.be.revertedWith( + "Proof already ended" + ) + }) + + it("requires no proofs when ending has not been extended", async function () { + let ending = (await currentTime()) + duration + await expect(await isProofRequiredBefore(id, ending)).to.be.true + let endingExtended = ending + hours(1) + await advanceTimeTo(periodEnd(periodOf(endingExtended) + 1)) + await expect(await isProofRequiredBefore(id, endingExtended)).to.be.false + }) + + it("requires proofs when ending has been extended", async function () { + let ending = (await currentTime()) + duration + await expect(await isProofRequiredBefore(id, ending)).to.be.true + let endingExtended = ending + hours(1) + await proofs.extendProofEndTo(id, endingExtended) + await expect(await isProofRequiredBefore(id, endingExtended)).to.be.true + }) + + it("no longer requires proofs after extension lapsed", async function () { + async function expectNoProofsForPeriods(id, periods) { + await advanceTimeTo(periodEnd(periodOf(await currentTime()))) + for (let i = 0; i < periods; i++) { + await expect(await proofs.isProofRequired(id)).to.be.false + await advanceTime(period) + } + } + + let ending = (await currentTime()) + duration + let endingExtended = ending + hours(1) + await proofs.extendProofEndTo(id, endingExtended) + await advanceTimeTo(periodEnd(periodOf(endingExtended) + 1)) + await expectNoProofsForPeriods(id, 100) + }) + }) }) diff --git a/test/Storage.test.js b/test/Storage.test.js index 59476ea..3217728 100644 --- a/test/Storage.test.js +++ b/test/Storage.test.js @@ -8,7 +8,11 @@ const { advanceTime, advanceTimeTo, currentTime, mine } = require("./evm") const { requestId, slotId } = require("./ids") const { periodic } = require("./time") const { price } = require("./price") -const { waitUntilExpired, waitUntilAllSlotsFilled } = require("./marketplace") +const { + waitUntilCancelled, + waitUntilStarted, + waitUntilFinished, +} = require("./marketplace") describe("Storage", function () { const proof = hexlify(randomBytes(42)) @@ -70,14 +74,9 @@ describe("Storage", function () { }) describe("ending the contract", function () { - async function waitUntilEnd() { - const end = (await storage.proofEnd(slotId(slot))).toNumber() - await advanceTimeTo(end) - } - it("unlocks the host collateral", async function () { await storage.fillSlot(slot.request, slot.index, proof) - await waitUntilEnd() + await waitUntilFinished(storage, slotId(slot)) await expect(storage.withdraw()).not.to.be.reverted }) }) @@ -178,7 +177,7 @@ describe("Storage", function () { it("fails to mark proof as missing when cancelled", async function () { await storage.fillSlot(slot.request, slot.index, proof) - await advanceTimeTo(request.expiry + 1) + await waitUntilCancelled(request.expiry) let missedPeriod = periodOf(await currentTime()) await expect( storage.markProofAsMissing(slotId(slot), missedPeriod) @@ -258,12 +257,7 @@ describe("Storage", function () { it("frees slot when collateral slashed below minimum threshold", async function () { const id = slotId(slot) - await waitUntilAllSlotsFilled( - storage, - request.ask.slots, - slot.request, - proof - ) + await waitUntilStarted(storage, request.ask.slots, slot.request, proof) while (true) { await markProofAsMissing(id) @@ -289,7 +283,7 @@ describe("Storage", function () { 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 waitUntilCancelled(request.expiry) await expect(await storage.isCancelled(slot.request)).to.equal(true) }) @@ -301,19 +295,14 @@ describe("Storage", function () { it("isSlotCancelled is true once request is cancelled", async function () { await storage.fillSlot(slot.request, slot.index, proof) - await waitUntilExpired(request.expiry) + await waitUntilCancelled(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 waitUntilStarted(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 diff --git a/test/marketplace.js b/test/marketplace.js index 35ceb3b..cfa1102 100644 --- a/test/marketplace.js +++ b/test/marketplace.js @@ -1,19 +1,33 @@ -const RequestState = { - New: 0, - Started: 1, - Cancelled: 2, - Finished: 3, - Failed: 4, +const { advanceTimeTo } = require("./evm") +const { slotId } = require("./ids") + +async function waitUntilCancelled(expiry) { + await advanceTimeTo(expiry + 1) } -async function waitUntilExpired(expiry) { - await ethers.provider.send("hardhat_mine", [ethers.utils.hexValue(expiry)]) -} - -async function waitUntilAllSlotsFilled(contract, numSlots, requestId, proof) { - for (let i = 0; i < numSlots; i++) { +async function waitUntilStarted(contract, numSlots, requestId, proof) { + const lastSlot = numSlots - 1 + for (let i = 0; i <= lastSlot; i++) { await contract.fillSlot(requestId, i, proof) } } -module.exports = { waitUntilExpired, waitUntilAllSlotsFilled, RequestState } +async function waitUntilFinished(contract, slotId) { + const end = (await contract.proofEnd(slotId)).toNumber() + await advanceTimeTo(end + 1) +} + +async function waitUntilFailed(contract, slot, maxSlotLoss) { + for (let i = 0; i <= maxSlotLoss; i++) { + slot.index = i + let id = slotId(slot) + await contract.freeSlot(id) + } +} + +module.exports = { + waitUntilCancelled, + waitUntilStarted, + waitUntilFinished, + waitUntilFailed, +}