[marketplace] extend proof ending
Allow proof ending to be extending once a contract is started, so that all filled slots share an ending time that is equal to the contract end time. Added tests. Add a mapping for proof id to endId so that proof expectations can be extended for all proofs that share a given endId. Add function modifiers that require the request state to allow proofs, with accompanying tests. General clean up of each function’s request state context, with accompanying tests. General clean up of all tests, including state change “wait” functions and normalising the time advancement functions.
This commit is contained in:
parent
1fff2f7295
commit
ad040cfee6
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
@ -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")
|
||||
})
|
||||
})
|
||||
|
||||
})
|
||||
})
|
||||
|
|
|
@ -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)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue