mirror of
https://github.com/status-im/dagger-contracts.git
synced 2025-01-12 23:35:49 +00:00
feat: hosts payed by actual time hosting a slot (#160)
Co-authored-by: Eric <5089238+emizzle@users.noreply.github.com> Co-authored-by: r4bbit <445106+0x-r4bbit@users.noreply.github.com>
This commit is contained in:
parent
f5a54c7ed4
commit
7e6187d4b1
@ -142,6 +142,22 @@ hook Sstore _requestContexts[KEY MarketplaceHarness.RequestId RequestId].endsAt
|
||||
| Helper functions |
|
||||
--------------------------------------------*/
|
||||
|
||||
function ensureValidRequestId(Marketplace.RequestId requestId) {
|
||||
// Without this, the prover will find counter examples with `requestId == 0`,
|
||||
// which are unlikely in practice as `requestId` is a hash from a request object.
|
||||
// However, `requestId == 0` enforces `SlotState.Free` in the `fillSlot` function regardless,
|
||||
// which ultimately results in counter examples where we have a state change
|
||||
// RequestState.Finished -> RequestState.Started, which is forbidden.
|
||||
//
|
||||
// COUNTER EXAMPLE: https://prover.certora.com/output/6199/81939b2b12d74a5cae5e84ceadb901c0?anonymousKey=a4ad6268598a1077ecfce75493b0c0f9bc3b17a0
|
||||
//
|
||||
// The `require` below is a hack to ensure we exclude such cases as the code
|
||||
// reverts in `requestIsKnown()` modifier (simply `require requestId != 0` isn't
|
||||
// sufficient here)
|
||||
// require requestId == to_bytes32(0) => currentContract._requests[requestId].client == 0;
|
||||
require requestId != to_bytes32(0) && currentContract._requests[requestId].client != 0;
|
||||
}
|
||||
|
||||
function canCancelRequest(method f) returns bool {
|
||||
return f.selector == sig:withdrawFunds(Marketplace.RequestId).selector;
|
||||
}
|
||||
@ -298,19 +314,7 @@ rule allowedRequestStateChanges(env e, method f) {
|
||||
// `SlotState.Finished` and `RequestState.New`
|
||||
requireInvariant finishedSlotAlwaysHasFinishedRequest(e, slotId);
|
||||
|
||||
// Without this, the prover will find counter examples with `requestId == 0`,
|
||||
// which are unlikely in practice as `requestId` is a hash from a request object.
|
||||
// However, `requestId == 0` enforces `SlotState.Free` in the `fillSlot` function regardless,
|
||||
// which ultimately results in counter examples where we have a state change
|
||||
// RequestState.Cancelled -> RequestState.Finished, which is forbidden.
|
||||
//
|
||||
// COUNTER EXAMPLE: https://prover.certora.com/output/6199/3a4f410e6367422ba60b218a08c04fae?anonymousKey=0d7003af4ee9bc18c0da0c80a216a6815d397370
|
||||
//
|
||||
// The `require` below is a hack to ensure we exclude such cases as the code
|
||||
// reverts in `requestIsKnown()` modifier (simply `require requestId != 0` isn't
|
||||
// sufficient here)
|
||||
require requestId == to_bytes32(0) => currentContract._requests[requestId].client == 0;
|
||||
|
||||
ensureValidRequestId(requestId);
|
||||
|
||||
Marketplace.RequestState requestStateBefore = currentContract.requestState(e, requestId);
|
||||
|
||||
@ -387,6 +391,8 @@ rule cancelledRequestsStayCancelled(env e, method f) {
|
||||
require requestStateBefore == Marketplace.RequestState.Cancelled;
|
||||
requireInvariant cancelledRequestAlwaysExpired(e, requestId);
|
||||
|
||||
ensureValidRequestId(requestId);
|
||||
|
||||
f(e, args);
|
||||
Marketplace.RequestState requestStateAfter = currentContract.requestState(e, requestId);
|
||||
|
||||
@ -398,18 +404,7 @@ rule finishedRequestsStayFinished(env e, method f) {
|
||||
calldataarg args;
|
||||
Marketplace.RequestId requestId;
|
||||
|
||||
// Without this, the prover will find counter examples with `requestId == 0`,
|
||||
// which are unlikely in practice as `requestId` is a hash from a request object.
|
||||
// However, `requestId == 0` enforces `SlotState.Free` in the `fillSlot` function regardless,
|
||||
// which ultimately results in counter examples where we have a state change
|
||||
// RequestState.Finished -> RequestState.Started, which is forbidden.
|
||||
//
|
||||
// COUNTER EXAMPLE: https://prover.certora.com/output/6199/81939b2b12d74a5cae5e84ceadb901c0?anonymousKey=a4ad6268598a1077ecfce75493b0c0f9bc3b17a0
|
||||
//
|
||||
// The `require` below is a hack to ensure we exclude such cases as the code
|
||||
// reverts in `requestIsKnown()` modifier (simply `require requestId != 0` isn't
|
||||
// sufficient here)
|
||||
require requestId == to_bytes32(0) => currentContract._requests[requestId].client == 0;
|
||||
ensureValidRequestId(requestId);
|
||||
|
||||
Marketplace.RequestState requestStateBefore = currentContract.requestState(e, requestId);
|
||||
require requestStateBefore == Marketplace.RequestState.Finished;
|
||||
|
@ -29,9 +29,14 @@ contract Marketplace is SlotReservations, Proofs, StateRetrieval, Endian {
|
||||
struct RequestContext {
|
||||
RequestState state;
|
||||
uint256 slotsFilled;
|
||||
/// @notice Tracks how much funds should be returned when Request expires to the Request creator
|
||||
/// @dev The sum is deducted every time a host fills a Slot by precalculated amount that he should receive if the Request expires
|
||||
uint256 expiryFundsWithdraw;
|
||||
/// @notice Tracks how much funds should be returned to the client as not all funds might be used for hosting the request
|
||||
/// @dev The sum starts with the full reward amount for the request and is reduced every time a host fills a slot.
|
||||
/// The reduction is calculated from the duration of time between the slot being filled and the request's end.
|
||||
/// This is the amount that will be paid out to the host when the request successfully finishes.
|
||||
/// @dev fundsToReturnToClient == 0 is used to signal that after request is terminated all the remaining funds were withdrawn.
|
||||
/// This is possible, because technically it is not possible for this variable to reach 0 in "natural" way as
|
||||
/// that would require all the slots to be filled at the same block as the request was created.
|
||||
uint256 fundsToReturnToClient;
|
||||
uint256 startedAt;
|
||||
uint256 endsAt;
|
||||
uint256 expiresAt;
|
||||
@ -41,7 +46,7 @@ contract Marketplace is SlotReservations, Proofs, StateRetrieval, Endian {
|
||||
SlotState state;
|
||||
RequestId requestId;
|
||||
/// @notice Timestamp that signals when slot was filled
|
||||
/// @dev Used for partial payouts when Requests expires and Hosts are paid out only the time they host the content.
|
||||
/// @dev Used for calculating payouts as hosts are paid based on time they actually host the content
|
||||
uint256 filledAt;
|
||||
uint256 slotIndex;
|
||||
/// @notice Tracks the current amount of host's collateral that is to be payed out at the end of Slot's lifespan.
|
||||
@ -112,8 +117,8 @@ contract Marketplace is SlotReservations, Proofs, StateRetrieval, Endian {
|
||||
|
||||
_addToMyRequests(request.client, id);
|
||||
|
||||
uint256 amount = request.price();
|
||||
_requestContexts[id].expiryFundsWithdraw = amount;
|
||||
uint256 amount = request.maxPrice();
|
||||
_requestContexts[id].fundsToReturnToClient = amount;
|
||||
_marketplaceTotals.received += amount;
|
||||
_transferFrom(msg.sender, amount);
|
||||
|
||||
@ -142,6 +147,7 @@ contract Marketplace is SlotReservations, Proofs, StateRetrieval, Endian {
|
||||
Slot storage slot = _slots[slotId];
|
||||
slot.requestId = requestId;
|
||||
slot.slotIndex = slotIndex;
|
||||
RequestContext storage context = _requestContexts[requestId];
|
||||
|
||||
require(slotState(slotId) == SlotState.Free, "Slot is not free");
|
||||
|
||||
@ -151,12 +157,9 @@ contract Marketplace is SlotReservations, Proofs, StateRetrieval, Endian {
|
||||
slot.host = msg.sender;
|
||||
slot.state = SlotState.Filled;
|
||||
slot.filledAt = block.timestamp;
|
||||
RequestContext storage context = _requestContexts[requestId];
|
||||
|
||||
context.slotsFilled += 1;
|
||||
context.expiryFundsWithdraw -= _expiryPayoutAmount(
|
||||
requestId,
|
||||
block.timestamp
|
||||
);
|
||||
context.fundsToReturnToClient -= _slotPayout(requestId, slot.filledAt);
|
||||
|
||||
// Collect collateral
|
||||
uint256 collateralAmount = request.ask.collateral;
|
||||
@ -288,8 +291,11 @@ contract Marketplace is SlotReservations, Proofs, StateRetrieval, Endian {
|
||||
RequestId requestId = slot.requestId;
|
||||
RequestContext storage context = _requestContexts[requestId];
|
||||
|
||||
_removeFromMySlots(slot.host, slotId);
|
||||
// We need to refund the amount of payout of the current node to the `fundsToReturnToClient` so
|
||||
// we keep correctly the track of the funds that needs to be returned at the end.
|
||||
context.fundsToReturnToClient += _slotPayout(requestId, slot.filledAt);
|
||||
|
||||
_removeFromMySlots(slot.host, slotId);
|
||||
uint256 slotIndex = slot.slotIndex;
|
||||
delete _slots[slotId];
|
||||
context.slotsFilled -= 1;
|
||||
@ -305,8 +311,6 @@ contract Marketplace is SlotReservations, Proofs, StateRetrieval, Endian {
|
||||
context.state = RequestState.Failed;
|
||||
context.endsAt = block.timestamp - 1;
|
||||
emit RequestFailed(requestId);
|
||||
|
||||
// TODO: send client remaining funds
|
||||
}
|
||||
}
|
||||
|
||||
@ -319,12 +323,12 @@ contract Marketplace is SlotReservations, Proofs, StateRetrieval, Endian {
|
||||
RequestContext storage context = _requestContexts[requestId];
|
||||
Request storage request = _requests[requestId];
|
||||
context.state = RequestState.Finished;
|
||||
_removeFromMyRequests(request.client, requestId);
|
||||
Slot storage slot = _slots[slotId];
|
||||
|
||||
_removeFromMyRequests(request.client, requestId);
|
||||
_removeFromMySlots(slot.host, slotId);
|
||||
|
||||
uint256 payoutAmount = _requests[requestId].pricePerSlot();
|
||||
uint256 payoutAmount = _slotPayout(requestId, slot.filledAt);
|
||||
uint256 collateralAmount = slot.currentCollateral;
|
||||
_marketplaceTotals.sent += (payoutAmount + collateralAmount);
|
||||
slot.state = SlotState.Paid;
|
||||
@ -350,7 +354,11 @@ contract Marketplace is SlotReservations, Proofs, StateRetrieval, Endian {
|
||||
Slot storage slot = _slots[slotId];
|
||||
_removeFromMySlots(slot.host, slotId);
|
||||
|
||||
uint256 payoutAmount = _expiryPayoutAmount(requestId, slot.filledAt);
|
||||
uint256 payoutAmount = _slotPayout(
|
||||
requestId,
|
||||
slot.filledAt,
|
||||
requestExpiry(requestId)
|
||||
);
|
||||
uint256 collateralAmount = slot.currentCollateral;
|
||||
_marketplaceTotals.sent += (payoutAmount + collateralAmount);
|
||||
slot.state = SlotState.Paid;
|
||||
@ -361,8 +369,8 @@ contract Marketplace is SlotReservations, Proofs, StateRetrieval, Endian {
|
||||
/**
|
||||
* @notice Withdraws remaining storage request funds back to the client that
|
||||
deposited them.
|
||||
* @dev Request must be expired, must be in RequestStat e.New, and the
|
||||
transaction must originate from the depositer address.
|
||||
* @dev Request must be cancelled, failed or finished, and the
|
||||
transaction must originate from the depositor address.
|
||||
* @param requestId the id of the request
|
||||
*/
|
||||
function withdrawFunds(RequestId requestId) public {
|
||||
@ -381,24 +389,46 @@ contract Marketplace is SlotReservations, Proofs, StateRetrieval, Endian {
|
||||
address withdrawRecipient
|
||||
) public {
|
||||
Request storage request = _requests[requestId];
|
||||
require(
|
||||
block.timestamp > requestExpiry(requestId),
|
||||
"Request not yet timed out"
|
||||
);
|
||||
require(request.client == msg.sender, "Invalid client address");
|
||||
RequestContext storage context = _requestContexts[requestId];
|
||||
require(context.state == RequestState.New, "Invalid state");
|
||||
RequestState state = requestState(requestId);
|
||||
require(
|
||||
state == RequestState.Cancelled ||
|
||||
state == RequestState.Failed ||
|
||||
state == RequestState.Finished,
|
||||
"Invalid state"
|
||||
);
|
||||
|
||||
// fundsToReturnToClient == 0 is used for "double-spend" protection, once the funds are withdrawn
|
||||
// then this variable is set to 0.
|
||||
require(context.fundsToReturnToClient != 0, "Nothing to withdraw");
|
||||
|
||||
if (state == RequestState.Cancelled) {
|
||||
context.state = RequestState.Cancelled;
|
||||
emit RequestCancelled(requestId);
|
||||
|
||||
// `fundsToReturnToClient` currently tracks funds to be returned for requests that successfully finish.
|
||||
// When requests are cancelled, funds earmarked for payment for the duration
|
||||
// between request expiry and request end (for every slot that was filled), should be returned to the client.
|
||||
// Update `fundsToReturnToClient` to reflect this.
|
||||
context.fundsToReturnToClient +=
|
||||
context.slotsFilled *
|
||||
_slotPayout(requestId, requestExpiry(requestId));
|
||||
} else if (state == RequestState.Failed) {
|
||||
// For Failed requests the client is refunded whole amount.
|
||||
context.fundsToReturnToClient = request.maxPrice();
|
||||
} else {
|
||||
context.state = RequestState.Finished;
|
||||
}
|
||||
|
||||
// Update request state to Cancelled. Handle in the withdraw transaction
|
||||
// as there needs to be someone to pay for the gas to update the state
|
||||
context.state = RequestState.Cancelled;
|
||||
_removeFromMyRequests(request.client, requestId);
|
||||
|
||||
emit RequestCancelled(requestId);
|
||||
|
||||
uint256 amount = context.expiryFundsWithdraw;
|
||||
uint256 amount = context.fundsToReturnToClient;
|
||||
_marketplaceTotals.sent += amount;
|
||||
assert(_token.transfer(withdrawRecipient, amount));
|
||||
|
||||
// We zero out the funds tracking in order to prevent double-spends
|
||||
context.fundsToReturnToClient = 0;
|
||||
}
|
||||
|
||||
function getActiveSlot(
|
||||
@ -442,24 +472,34 @@ contract Marketplace is SlotReservations, Proofs, StateRetrieval, Endian {
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Calculates the amount that should be paid out to a host if a request
|
||||
* expires based on when the host fills the slot
|
||||
* @notice Calculates the amount that should be paid out to a host that successfully finished the request
|
||||
* @param requestId RequestId of the request used to calculate the payout
|
||||
* amount.
|
||||
* @param startingTimestamp timestamp indicating when a host filled a slot and
|
||||
* started providing proofs.
|
||||
*/
|
||||
function _expiryPayoutAmount(
|
||||
function _slotPayout(
|
||||
RequestId requestId,
|
||||
uint256 startingTimestamp
|
||||
) private view returns (uint256) {
|
||||
Request storage request = _requests[requestId];
|
||||
require(
|
||||
startingTimestamp < requestExpiry(requestId),
|
||||
"Start not before expiry"
|
||||
);
|
||||
return
|
||||
_slotPayout(
|
||||
requestId,
|
||||
startingTimestamp,
|
||||
_requestContexts[requestId].endsAt
|
||||
);
|
||||
}
|
||||
|
||||
return (requestExpiry(requestId) - startingTimestamp) * request.ask.reward;
|
||||
/// @notice Calculates the amount that should be paid out to a host based on the specified time frame.
|
||||
function _slotPayout(
|
||||
RequestId requestId,
|
||||
uint256 startingTimestamp,
|
||||
uint256 endingTimestamp
|
||||
) private view returns (uint256) {
|
||||
Request storage request = _requests[requestId];
|
||||
require(startingTimestamp < endingTimestamp, "Start not before expiry");
|
||||
|
||||
return (endingTimestamp - startingTimestamp) * request.ask.reward;
|
||||
}
|
||||
|
||||
function getHost(SlotId slotId) public view returns (address) {
|
||||
|
@ -74,13 +74,7 @@ library Requests {
|
||||
}
|
||||
}
|
||||
|
||||
function pricePerSlot(
|
||||
Request memory request
|
||||
) internal pure returns (uint256) {
|
||||
return request.ask.duration * request.ask.reward;
|
||||
}
|
||||
|
||||
function price(Request memory request) internal pure returns (uint256) {
|
||||
return request.ask.slots * pricePerSlot(request);
|
||||
function maxPrice(Request memory request) internal pure returns (uint256) {
|
||||
return request.ask.slots * request.ask.duration * request.ask.reward;
|
||||
}
|
||||
}
|
||||
|
@ -23,7 +23,7 @@ const {
|
||||
waitUntilSlotFailed,
|
||||
patchOverloads,
|
||||
} = require("./marketplace")
|
||||
const { price, pricePerSlot } = require("./price")
|
||||
const { maxPrice, payoutForDuration } = require("./price")
|
||||
const {
|
||||
snapshot,
|
||||
revert,
|
||||
@ -165,7 +165,7 @@ describe("Marketplace", function () {
|
||||
})
|
||||
|
||||
it("emits event when storage is requested", async function () {
|
||||
await token.approve(marketplace.address, price(request))
|
||||
await token.approve(marketplace.address, maxPrice(request))
|
||||
|
||||
// We +1 second to the expiry because the time will advance with the mined transaction for requestStorage because of Hardhat
|
||||
const expectedExpiry = (await currentTime()) + request.expiry + 1
|
||||
@ -175,7 +175,7 @@ describe("Marketplace", function () {
|
||||
})
|
||||
|
||||
it("allows retrieval of request details", async function () {
|
||||
await token.approve(marketplace.address, price(request))
|
||||
await token.approve(marketplace.address, maxPrice(request))
|
||||
await marketplace.requestStorage(request)
|
||||
const id = requestId(request)
|
||||
expect(await marketplace.getRequest(id)).to.be.request(request)
|
||||
@ -183,14 +183,14 @@ describe("Marketplace", function () {
|
||||
|
||||
it("rejects request with invalid client address", async function () {
|
||||
let invalid = { ...request, client: host.address }
|
||||
await token.approve(marketplace.address, price(invalid))
|
||||
await token.approve(marketplace.address, maxPrice(invalid))
|
||||
await expect(marketplace.requestStorage(invalid)).to.be.revertedWith(
|
||||
"Invalid client address"
|
||||
)
|
||||
})
|
||||
|
||||
it("rejects request with insufficient payment", async function () {
|
||||
let insufficient = price(request) - 1
|
||||
let insufficient = maxPrice(request) - 1
|
||||
await token.approve(marketplace.address, insufficient)
|
||||
await expect(marketplace.requestStorage(request)).to.be.revertedWith(
|
||||
"ERC20: insufficient allowance"
|
||||
@ -198,7 +198,7 @@ describe("Marketplace", function () {
|
||||
})
|
||||
|
||||
it("rejects request when expiry out of bounds", async function () {
|
||||
await token.approve(marketplace.address, price(request))
|
||||
await token.approve(marketplace.address, maxPrice(request))
|
||||
|
||||
request.expiry = request.ask.duration + 1
|
||||
await expect(marketplace.requestStorage(request)).to.be.revertedWith(
|
||||
@ -226,7 +226,7 @@ describe("Marketplace", function () {
|
||||
})
|
||||
|
||||
it("rejects resubmission of request", async function () {
|
||||
await token.approve(marketplace.address, price(request) * 2)
|
||||
await token.approve(marketplace.address, maxPrice(request) * 2)
|
||||
await marketplace.requestStorage(request)
|
||||
await expect(marketplace.requestStorage(request)).to.be.revertedWith(
|
||||
"Request already exists"
|
||||
@ -237,7 +237,7 @@ describe("Marketplace", function () {
|
||||
describe("filling a slot with collateral", function () {
|
||||
beforeEach(async function () {
|
||||
switchAccount(client)
|
||||
await token.approve(marketplace.address, price(request))
|
||||
await token.approve(marketplace.address, maxPrice(request))
|
||||
await marketplace.requestStorage(request)
|
||||
switchAccount(host)
|
||||
await token.approve(marketplace.address, request.ask.collateral)
|
||||
@ -296,7 +296,7 @@ describe("Marketplace", function () {
|
||||
it("is rejected when request is cancelled", async function () {
|
||||
switchAccount(client)
|
||||
let expired = { ...request, expiry: hours(1) + 1 }
|
||||
await token.approve(marketplace.address, price(request))
|
||||
await token.approve(marketplace.address, maxPrice(request))
|
||||
await marketplace.requestStorage(expired)
|
||||
await waitUntilCancelled(expired)
|
||||
switchAccount(host)
|
||||
@ -335,7 +335,7 @@ describe("Marketplace", function () {
|
||||
marketplace.address,
|
||||
request.ask.collateral * lastSlot
|
||||
)
|
||||
await token.approve(marketplace.address, price(request) * lastSlot)
|
||||
await token.approve(marketplace.address, maxPrice(request) * lastSlot)
|
||||
for (let i = 0; i <= lastSlot; i++) {
|
||||
await marketplace.reserveSlot(slot.request, i)
|
||||
await marketplace.fillSlot(slot.request, i, proof)
|
||||
@ -355,7 +355,7 @@ describe("Marketplace", function () {
|
||||
describe("filling slot without collateral", function () {
|
||||
beforeEach(async function () {
|
||||
switchAccount(client)
|
||||
await token.approve(marketplace.address, price(request))
|
||||
await token.approve(marketplace.address, maxPrice(request))
|
||||
await marketplace.requestStorage(request)
|
||||
switchAccount(host)
|
||||
})
|
||||
@ -382,7 +382,7 @@ describe("Marketplace", function () {
|
||||
describe("submitting proofs when slot is filled", function () {
|
||||
beforeEach(async function () {
|
||||
switchAccount(client)
|
||||
await token.approve(marketplace.address, price(request))
|
||||
await token.approve(marketplace.address, maxPrice(request))
|
||||
await marketplace.requestStorage(request)
|
||||
switchAccount(host)
|
||||
await token.approve(marketplace.address, request.ask.collateral)
|
||||
@ -419,7 +419,7 @@ describe("Marketplace", function () {
|
||||
var requestTime
|
||||
beforeEach(async function () {
|
||||
switchAccount(client)
|
||||
await token.approve(marketplace.address, price(request))
|
||||
await token.approve(marketplace.address, maxPrice(request))
|
||||
await marketplace.requestStorage(request)
|
||||
requestTime = await currentTime()
|
||||
switchAccount(host)
|
||||
@ -477,7 +477,7 @@ describe("Marketplace", function () {
|
||||
id = slotId(slot)
|
||||
|
||||
switchAccount(client)
|
||||
await token.approve(marketplace.address, price(request))
|
||||
await token.approve(marketplace.address, maxPrice(request))
|
||||
await marketplace.requestStorage(request)
|
||||
switchAccount(host)
|
||||
await token.approve(marketplace.address, request.ask.collateral)
|
||||
@ -515,52 +515,84 @@ describe("Marketplace", function () {
|
||||
describe("paying out a slot", function () {
|
||||
beforeEach(async function () {
|
||||
switchAccount(client)
|
||||
await token.approve(marketplace.address, price(request))
|
||||
await token.approve(marketplace.address, maxPrice(request))
|
||||
await marketplace.requestStorage(request)
|
||||
switchAccount(host)
|
||||
await token.approve(marketplace.address, request.ask.collateral)
|
||||
})
|
||||
|
||||
it("pays the host when contract has finished and returns collateral", async function () {
|
||||
await waitUntilStarted(marketplace, request, proof, token)
|
||||
it("finished request pays out reward based on time hosted", async function () {
|
||||
// We are advancing the time because most of the slots will be filled somewhere
|
||||
// in the "expiry window" and not at its beginning. This is more "real" setup
|
||||
// and demonstrates the partial payout feature better.
|
||||
await advanceTimeForNextBlock(request.expiry / 2)
|
||||
|
||||
const expectedPayouts = await waitUntilStarted(
|
||||
marketplace,
|
||||
request,
|
||||
proof,
|
||||
token
|
||||
)
|
||||
await waitUntilFinished(marketplace, requestId(request))
|
||||
const startBalance = await token.balanceOf(host.address)
|
||||
|
||||
const startBalanceHost = await token.balanceOf(host.address)
|
||||
await marketplace.freeSlot(slotId(slot))
|
||||
const endBalance = await token.balanceOf(host.address)
|
||||
expect(endBalance - startBalance).to.equal(
|
||||
pricePerSlot(request) + request.ask.collateral
|
||||
const endBalanceHost = await token.balanceOf(host.address)
|
||||
|
||||
expect(expectedPayouts[slot.index]).to.be.lt(maxPrice(request))
|
||||
expect(endBalanceHost - startBalanceHost).to.equal(
|
||||
expectedPayouts[slot.index] + request.ask.collateral
|
||||
)
|
||||
})
|
||||
|
||||
it("pays to host reward address when contract has finished and returns collateral to host collateral address", async function () {
|
||||
it("returns collateral to host collateral address if specified", async function () {
|
||||
await waitUntilStarted(marketplace, request, proof, token)
|
||||
await waitUntilFinished(marketplace, requestId(request))
|
||||
|
||||
const startBalanceHost = await token.balanceOf(host.address)
|
||||
const startBalanceReward = await token.balanceOf(
|
||||
hostRewardRecipient.address
|
||||
)
|
||||
const startBalanceCollateral = await token.balanceOf(
|
||||
hostCollateralRecipient.address
|
||||
)
|
||||
|
||||
await marketplace.freeSlot(
|
||||
slotId(slot),
|
||||
hostRewardRecipient.address,
|
||||
hostCollateralRecipient.address
|
||||
)
|
||||
const endBalanceHost = await token.balanceOf(host.address)
|
||||
const endBalanceReward = await token.balanceOf(
|
||||
hostRewardRecipient.address
|
||||
)
|
||||
|
||||
const endBalanceCollateral = await token.balanceOf(
|
||||
hostCollateralRecipient.address
|
||||
)
|
||||
|
||||
const endBalanceHost = await token.balanceOf(host.address)
|
||||
expect(endBalanceHost).to.equal(startBalanceHost)
|
||||
expect(endBalanceCollateral - startBalanceCollateral).to.equal(
|
||||
request.ask.collateral
|
||||
)
|
||||
expect(endBalanceReward - startBalanceReward).to.equal(
|
||||
pricePerSlot(request)
|
||||
})
|
||||
|
||||
it("pays reward to host reward address if specified", async function () {
|
||||
await waitUntilStarted(marketplace, request, proof, token)
|
||||
await waitUntilFinished(marketplace, requestId(request))
|
||||
|
||||
const startBalanceHost = await token.balanceOf(host.address)
|
||||
const startBalanceReward = await token.balanceOf(
|
||||
hostRewardRecipient.address
|
||||
)
|
||||
|
||||
await marketplace.freeSlot(
|
||||
slotId(slot),
|
||||
hostRewardRecipient.address,
|
||||
hostCollateralRecipient.address
|
||||
)
|
||||
|
||||
const endBalanceHost = await token.balanceOf(host.address)
|
||||
const endBalanceReward = await token.balanceOf(
|
||||
hostRewardRecipient.address
|
||||
)
|
||||
|
||||
expect(endBalanceHost).to.equal(startBalanceHost)
|
||||
expect(endBalanceReward - startBalanceReward).to.gt(0)
|
||||
})
|
||||
|
||||
it("pays the host when contract was cancelled", async function () {
|
||||
@ -671,7 +703,7 @@ describe("Marketplace", function () {
|
||||
describe("fulfilling a request", function () {
|
||||
beforeEach(async function () {
|
||||
switchAccount(client)
|
||||
await token.approve(marketplace.address, price(request))
|
||||
await token.approve(marketplace.address, maxPrice(request))
|
||||
await marketplace.requestStorage(request)
|
||||
switchAccount(host)
|
||||
await token.approve(marketplace.address, request.ask.collateral)
|
||||
@ -724,7 +756,7 @@ describe("Marketplace", function () {
|
||||
describe("withdrawing funds", function () {
|
||||
beforeEach(async function () {
|
||||
switchAccount(client)
|
||||
await token.approve(marketplace.address, price(request))
|
||||
await token.approve(marketplace.address, maxPrice(request))
|
||||
await marketplace.requestStorage(request)
|
||||
switchAccount(host)
|
||||
await token.approve(marketplace.address, request.ask.collateral)
|
||||
@ -734,7 +766,7 @@ describe("Marketplace", function () {
|
||||
switchAccount(client)
|
||||
await expect(
|
||||
marketplace.withdrawFunds(slot.request, clientWithdrawRecipient.address)
|
||||
).to.be.revertedWith("Request not yet timed out")
|
||||
).to.be.revertedWith("Invalid state")
|
||||
})
|
||||
|
||||
it("rejects withdraw when wrong account used", async function () {
|
||||
@ -762,6 +794,20 @@ describe("Marketplace", function () {
|
||||
).to.be.revertedWith("Invalid state")
|
||||
})
|
||||
|
||||
it("rejects withdraw when already withdrawn", async function () {
|
||||
await waitUntilStarted(marketplace, request, proof, token)
|
||||
await waitUntilFinished(marketplace, requestId(request))
|
||||
|
||||
switchAccount(client)
|
||||
await marketplace.withdrawFunds(
|
||||
slot.request,
|
||||
clientWithdrawRecipient.address
|
||||
)
|
||||
await expect(
|
||||
marketplace.withdrawFunds(slot.request, clientWithdrawRecipient.address)
|
||||
).to.be.revertedWith("Nothing to withdraw")
|
||||
})
|
||||
|
||||
it("emits event once request is cancelled", async function () {
|
||||
await waitUntilCancelled(request)
|
||||
switchAccount(client)
|
||||
@ -772,7 +818,37 @@ describe("Marketplace", function () {
|
||||
.withArgs(requestId(request))
|
||||
})
|
||||
|
||||
it("withdraws to the client payout address", async function () {
|
||||
it("withdraw rest of funds to the client payout address for finished requests", async function () {
|
||||
await waitUntilStarted(marketplace, request, proof, token)
|
||||
await waitUntilFinished(marketplace, requestId(request))
|
||||
|
||||
switchAccount(client)
|
||||
const startBalanceClient = await token.balanceOf(client.address)
|
||||
const startBalancePayout = await token.balanceOf(
|
||||
clientWithdrawRecipient.address
|
||||
)
|
||||
await marketplace.withdrawFunds(
|
||||
slot.request,
|
||||
clientWithdrawRecipient.address
|
||||
)
|
||||
|
||||
const endBalanceClient = await token.balanceOf(client.address)
|
||||
const endBalancePayout = await token.balanceOf(
|
||||
clientWithdrawRecipient.address
|
||||
)
|
||||
|
||||
expect(endBalanceClient).to.equal(startBalanceClient)
|
||||
// As all the request's slots will get filled and request will start and successfully finishes,
|
||||
// then the upper bound to how much the client gets returned is the cumulative reward for all the
|
||||
// slots for expiry window. This limit is "inclusive" because it is possible that all slots are filled
|
||||
// at the time of expiry and hence the user would get the full "expiry window" reward back.
|
||||
expect(endBalancePayout - startBalancePayout).to.be.gt(0)
|
||||
expect(endBalancePayout - startBalancePayout).to.be.lte(
|
||||
request.expiry * request.ask.reward
|
||||
)
|
||||
})
|
||||
|
||||
it("withdraws to the client payout address when request is cancelled", async function () {
|
||||
await waitUntilCancelled(request)
|
||||
switchAccount(client)
|
||||
const startBalanceClient = await token.balanceOf(client.address)
|
||||
@ -788,7 +864,31 @@ describe("Marketplace", function () {
|
||||
clientWithdrawRecipient.address
|
||||
)
|
||||
expect(endBalanceClient).to.equal(startBalanceClient)
|
||||
expect(endBalancePayout - startBalancePayout).to.equal(price(request))
|
||||
expect(endBalancePayout - startBalancePayout).to.equal(maxPrice(request))
|
||||
})
|
||||
|
||||
it("withdraws full price for failed requests to the client payout address", async function () {
|
||||
await waitUntilStarted(marketplace, request, proof, token)
|
||||
await waitUntilFailed(marketplace, request)
|
||||
|
||||
switchAccount(client)
|
||||
|
||||
const startBalanceClient = await token.balanceOf(client.address)
|
||||
const startBalancePayout = await token.balanceOf(
|
||||
clientWithdrawRecipient.address
|
||||
)
|
||||
await marketplace.withdrawFunds(
|
||||
slot.request,
|
||||
clientWithdrawRecipient.address
|
||||
)
|
||||
|
||||
const endBalanceClient = await token.balanceOf(client.address)
|
||||
const endBalancePayout = await token.balanceOf(
|
||||
clientWithdrawRecipient.address
|
||||
)
|
||||
|
||||
expect(endBalanceClient).to.equal(startBalanceClient)
|
||||
expect(endBalancePayout - startBalancePayout).to.equal(maxPrice(request))
|
||||
})
|
||||
|
||||
it("withdraws to the client payout address for cancelled requests lowered by hosts payout", async function () {
|
||||
@ -812,7 +912,29 @@ describe("Marketplace", function () {
|
||||
)
|
||||
const endBalance = await token.balanceOf(clientWithdrawRecipient.address)
|
||||
expect(endBalance - ACCOUNT_STARTING_BALANCE).to.equal(
|
||||
price(request) - expectedPartialhostRewardRecipient
|
||||
maxPrice(request) - expectedPartialhostRewardRecipient
|
||||
)
|
||||
})
|
||||
|
||||
it("when slot is freed and not repaired, client will get refunded the freed slot's funds", async function () {
|
||||
const payouts = await waitUntilStarted(marketplace, request, proof, token)
|
||||
|
||||
await expect(marketplace.freeSlot(slotId(slot))).to.emit(
|
||||
marketplace,
|
||||
"SlotFreed"
|
||||
)
|
||||
await waitUntilFinished(marketplace, requestId(request))
|
||||
|
||||
switchAccount(client)
|
||||
await marketplace.withdrawFunds(
|
||||
slot.request,
|
||||
clientWithdrawRecipient.address
|
||||
)
|
||||
const endBalance = await token.balanceOf(clientWithdrawRecipient.address)
|
||||
expect(endBalance - ACCOUNT_STARTING_BALANCE).to.equal(
|
||||
maxPrice(request) -
|
||||
payouts.reduce((a, b) => a + b, 0) + // This is the amount that user gets refunded for filling period in expiry window
|
||||
payouts[slot.index] // This is the refunded amount for the freed slot
|
||||
)
|
||||
})
|
||||
})
|
||||
@ -822,7 +944,7 @@ describe("Marketplace", function () {
|
||||
|
||||
beforeEach(async function () {
|
||||
switchAccount(client)
|
||||
await token.approve(marketplace.address, price(request))
|
||||
await token.approve(marketplace.address, maxPrice(request))
|
||||
await marketplace.requestStorage(request)
|
||||
switchAccount(host)
|
||||
await token.approve(marketplace.address, request.ask.collateral)
|
||||
@ -901,7 +1023,7 @@ describe("Marketplace", function () {
|
||||
;({ periodOf, periodEnd } = periodic(period))
|
||||
|
||||
switchAccount(client)
|
||||
await token.approve(marketplace.address, price(request))
|
||||
await token.approve(marketplace.address, maxPrice(request))
|
||||
await marketplace.requestStorage(request)
|
||||
switchAccount(host)
|
||||
await token.approve(marketplace.address, request.ask.collateral)
|
||||
@ -988,7 +1110,7 @@ describe("Marketplace", function () {
|
||||
;({ periodOf, periodEnd } = periodic(period))
|
||||
|
||||
switchAccount(client)
|
||||
await token.approve(marketplace.address, price(request))
|
||||
await token.approve(marketplace.address, maxPrice(request))
|
||||
await marketplace.requestStorage(request)
|
||||
switchAccount(host)
|
||||
await token.approve(marketplace.address, request.ask.collateral)
|
||||
@ -1079,7 +1201,7 @@ describe("Marketplace", function () {
|
||||
;({ periodOf, periodEnd } = periodic(period))
|
||||
|
||||
switchAccount(client)
|
||||
await token.approve(marketplace.address, price(request))
|
||||
await token.approve(marketplace.address, maxPrice(request))
|
||||
await marketplace.requestStorage(request)
|
||||
switchAccount(host)
|
||||
await token.approve(marketplace.address, request.ask.collateral)
|
||||
@ -1190,7 +1312,7 @@ describe("Marketplace", function () {
|
||||
switchAccount(host)
|
||||
await token.approve(marketplace.address, request.ask.collateral)
|
||||
switchAccount(client)
|
||||
await token.approve(marketplace.address, price(request))
|
||||
await token.approve(marketplace.address, maxPrice(request))
|
||||
})
|
||||
|
||||
it("adds request to list when requesting storage", async function () {
|
||||
@ -1239,7 +1361,7 @@ describe("Marketplace", function () {
|
||||
describe("list of active slots", function () {
|
||||
beforeEach(async function () {
|
||||
switchAccount(client)
|
||||
await token.approve(marketplace.address, price(request))
|
||||
await token.approve(marketplace.address, maxPrice(request))
|
||||
await marketplace.requestStorage(request)
|
||||
switchAccount(host)
|
||||
await token.approve(marketplace.address, request.ask.collateral)
|
||||
|
@ -1,6 +1,6 @@
|
||||
const { advanceTimeToForNextBlock, currentTime } = require("./evm")
|
||||
const { slotId, requestId } = require("./ids")
|
||||
const { price } = require("./price")
|
||||
const { maxPrice, payoutForDuration } = require("./price")
|
||||
|
||||
/**
|
||||
* @dev This will not advance the time right on the "expiry threshold" but will most probably "overshoot it"
|
||||
@ -14,13 +14,33 @@ async function waitUntilCancelled(request) {
|
||||
await advanceTimeToForNextBlock((await currentTime()) + request.expiry + 1)
|
||||
}
|
||||
|
||||
async function waitUntilStarted(contract, request, proof, token) {
|
||||
await token.approve(contract.address, price(request) * request.ask.slots)
|
||||
async function waitUntilSlotsFilled(contract, request, proof, token, slots) {
|
||||
await token.approve(contract.address, request.ask.collateral * slots.length)
|
||||
|
||||
for (let i = 0; i < request.ask.slots; i++) {
|
||||
await contract.reserveSlot(requestId(request), i)
|
||||
await contract.fillSlot(requestId(request), i, proof)
|
||||
let requestEnd = (await contract.requestEnd(requestId(request))).toNumber()
|
||||
const payouts = []
|
||||
for (let slotIndex of slots) {
|
||||
await contract.reserveSlot(requestId(request), slotIndex)
|
||||
await contract.fillSlot(requestId(request), slotIndex, proof)
|
||||
|
||||
payouts[slotIndex] = payoutForDuration(
|
||||
request,
|
||||
await currentTime(),
|
||||
requestEnd
|
||||
)
|
||||
}
|
||||
|
||||
return payouts
|
||||
}
|
||||
|
||||
async function waitUntilStarted(contract, request, proof, token) {
|
||||
return waitUntilSlotsFilled(
|
||||
contract,
|
||||
request,
|
||||
proof,
|
||||
token,
|
||||
Array.from({ length: request.ask.slots }, (_, i) => i)
|
||||
)
|
||||
}
|
||||
|
||||
async function waitUntilFinished(contract, requestId) {
|
||||
@ -83,6 +103,7 @@ function patchOverloads(contract) {
|
||||
module.exports = {
|
||||
waitUntilCancelled,
|
||||
waitUntilStarted,
|
||||
waitUntilSlotsFilled,
|
||||
waitUntilFinished,
|
||||
waitUntilFailed,
|
||||
waitUntilSlotFailed,
|
||||
|
@ -1,9 +1,9 @@
|
||||
function price(request) {
|
||||
return request.ask.slots * pricePerSlot(request)
|
||||
function maxPrice(request) {
|
||||
return request.ask.slots * request.ask.duration * request.ask.reward
|
||||
}
|
||||
|
||||
function pricePerSlot(request) {
|
||||
return request.ask.duration * request.ask.reward
|
||||
function payoutForDuration(request, start, end) {
|
||||
return (end - start) * request.ask.reward
|
||||
}
|
||||
|
||||
module.exports = { price, pricePerSlot }
|
||||
module.exports = { maxPrice, payoutForDuration }
|
||||
|
@ -18,7 +18,7 @@ const SlotState = {
|
||||
Cancelled: 5,
|
||||
}
|
||||
|
||||
const enableRequestAssertions = function () {
|
||||
function enableRequestAssertions() {
|
||||
// language chain method
|
||||
Assertion.addMethod("request", function (request) {
|
||||
var actual = this._obj
|
||||
|
Loading…
x
Reference in New Issue
Block a user