feat: expiry specified as duration (#99)

This commit is contained in:
Adam Uhlíř 2024-05-06 15:13:32 +02:00 committed by GitHub
parent 4d9320a582
commit 57e8cd5013
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 72 additions and 32 deletions

View File

@ -32,6 +32,7 @@ contract Marketplace is Proofs, StateRetrieval, Endian {
uint256 expiryFundsWithdraw; uint256 expiryFundsWithdraw;
uint256 startedAt; uint256 startedAt;
uint256 endsAt; uint256 endsAt;
uint256 expiresAt;
} }
struct Slot { struct Slot {
@ -90,11 +91,14 @@ contract Marketplace is Proofs, StateRetrieval, Endian {
RequestId id = request.id(); RequestId id = request.id();
require(_requests[id].client == address(0), "Request already exists"); require(_requests[id].client == address(0), "Request already exists");
require(
request.expiry > 0 && request.expiry < request.ask.duration,
"Expiry not in range"
);
_requests[id] = request; _requests[id] = request;
uint256 endsAt = block.timestamp + request.ask.duration; _requestContexts[id].endsAt = block.timestamp + request.ask.duration;
require(endsAt > request.expiry, "Request end before expiry"); _requestContexts[id].expiresAt = block.timestamp + request.expiry;
_requestContexts[id].endsAt = endsAt;
_addToMyRequests(request.client, id); _addToMyRequests(request.client, id);
@ -103,7 +107,7 @@ contract Marketplace is Proofs, StateRetrieval, Endian {
_marketplaceTotals.received += amount; _marketplaceTotals.received += amount;
_transferFrom(msg.sender, amount); _transferFrom(msg.sender, amount);
emit StorageRequested(id, request.ask, request.expiry); emit StorageRequested(id, request.ask, _requestContexts[id].expiresAt);
} }
function fillSlot( function fillSlot(
@ -206,6 +210,8 @@ contract Marketplace is Proofs, StateRetrieval, Endian {
Slot storage slot = _slots[slotId]; Slot storage slot = _slots[slotId];
Request storage request = _requests[slot.requestId]; Request storage request = _requests[slot.requestId];
// TODO: Reward for validator that calls this function
if (missingProofs(slotId) % _config.collateral.slashCriterion == 0) { if (missingProofs(slotId) % _config.collateral.slashCriterion == 0) {
uint256 slashedAmount = (request.ask.collateral * uint256 slashedAmount = (request.ask.collateral *
_config.collateral.slashPercentage) / 100; _config.collateral.slashPercentage) / 100;
@ -286,7 +292,10 @@ contract Marketplace is Proofs, StateRetrieval, Endian {
/// @param requestId the id of the request /// @param requestId the id of the request
function withdrawFunds(RequestId requestId) public { function withdrawFunds(RequestId requestId) public {
Request storage request = _requests[requestId]; Request storage request = _requests[requestId];
require(block.timestamp > request.expiry, "Request not yet timed out"); require(
block.timestamp > requestExpiry(requestId),
"Request not yet timed out"
);
require(request.client == msg.sender, "Invalid client address"); require(request.client == msg.sender, "Invalid client address");
RequestContext storage context = _requestContexts[requestId]; RequestContext storage context = _requestContexts[requestId];
require(context.state == RequestState.New, "Invalid state"); require(context.state == RequestState.New, "Invalid state");
@ -339,15 +348,22 @@ contract Marketplace is Proofs, StateRetrieval, Endian {
} }
} }
function requestExpiry(RequestId requestId) public view returns (uint256) {
return _requestContexts[requestId].expiresAt;
}
/// @notice Calculates the amount that should be payed out to a host if a request expires based on when the host fills the slot /// @notice Calculates the amount that should be payed out to a host if a request expires based on when the host fills the slot
function _expiryPayoutAmount( function _expiryPayoutAmount(
RequestId requestId, RequestId requestId,
uint256 startingTimestamp uint256 startingTimestamp
) private view returns (uint256) { ) private view returns (uint256) {
Request storage request = _requests[requestId]; Request storage request = _requests[requestId];
require(startingTimestamp < request.expiry, "Start not before expiry"); require(
startingTimestamp < requestExpiry(requestId),
"Start not before expiry"
);
return (request.expiry - startingTimestamp) * request.ask.reward; return (requestExpiry(requestId) - startingTimestamp) * request.ask.reward;
} }
function getHost(SlotId slotId) public view returns (address) { function getHost(SlotId slotId) public view returns (address) {
@ -360,7 +376,7 @@ contract Marketplace is Proofs, StateRetrieval, Endian {
RequestContext storage context = _requestContexts[requestId]; RequestContext storage context = _requestContexts[requestId];
if ( if (
context.state == RequestState.New && context.state == RequestState.New &&
block.timestamp > _requests[requestId].expiry block.timestamp > requestExpiry(requestId)
) { ) {
return RequestState.Cancelled; return RequestState.Cancelled;
} else if ( } else if (

View File

@ -8,7 +8,7 @@ struct Request {
address client; address client;
Ask ask; Ask ask;
Content content; Content content;
uint256 expiry; // timestamp as seconds since unix epoch at which this request expires uint256 expiry; // amount of seconds since start of the request at which this request expires
bytes32 nonce; // random nonce to differentiate between similar requests bytes32 nonce; // random nonce to differentiate between similar requests
} }

View File

@ -140,9 +140,12 @@ describe("Marketplace", function () {
it("emits event when storage is requested", async function () { it("emits event when storage is requested", async function () {
await token.approve(marketplace.address, price(request)) await token.approve(marketplace.address, price(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
await expect(marketplace.requestStorage(request)) await expect(marketplace.requestStorage(request))
.to.emit(marketplace, "StorageRequested") .to.emit(marketplace, "StorageRequested")
.withArgs(requestId(request), askToArray(request.ask), request.expiry) .withArgs(requestId(request), askToArray(request.ask), expectedExpiry)
}) })
it("allows retrieval of request details", async function () { it("allows retrieval of request details", async function () {
@ -168,11 +171,17 @@ describe("Marketplace", function () {
) )
}) })
it("rejects request when expiry is after request end", async function () { it("rejects request when expiry out of bounds", async function () {
request.expiry = (await currentTime()) + request.ask.duration + hours(1)
await token.approve(marketplace.address, price(request)) await token.approve(marketplace.address, price(request))
request.expiry = request.ask.duration + 1
await expect(marketplace.requestStorage(request)).to.be.revertedWith( await expect(marketplace.requestStorage(request)).to.be.revertedWith(
"Request end before expiry" "Expiry not in range"
)
request.expiry = 0
await expect(marketplace.requestStorage(request)).to.be.revertedWith(
"Expiry not in range"
) )
}) })
@ -241,9 +250,10 @@ describe("Marketplace", function () {
it("is rejected when request is cancelled", async function () { it("is rejected when request is cancelled", async function () {
switchAccount(client) switchAccount(client)
let expired = { ...request, expiry: (await currentTime()) - hours(1) } let expired = { ...request, expiry: hours(1) + 1 }
await token.approve(marketplace.address, price(request)) await token.approve(marketplace.address, price(request))
await marketplace.requestStorage(expired) await marketplace.requestStorage(expired)
await waitUntilCancelled(expired)
switchAccount(host) switchAccount(host)
await expect( await expect(
marketplace.fillSlot(requestId(expired), slot.index, proof) marketplace.fillSlot(requestId(expired), slot.index, proof)
@ -465,20 +475,19 @@ describe("Marketplace", function () {
}) })
it("pays the host when contract was cancelled", async function () { it("pays the host when contract was cancelled", async function () {
// Lets move the time into middle of the expiry window // Lets advance the time more into the expiry window
const fillTimestamp = const filledAt = (await currentTime()) + Math.floor(request.expiry / 3)
(await currentTime()) + const expiresAt = (
Math.floor((request.expiry - (await currentTime())) / 2) - await marketplace.requestExpiry(requestId(request))
1 ).toNumber()
await advanceTimeToForNextBlock(fillTimestamp)
await advanceTimeToForNextBlock(filledAt)
await marketplace.fillSlot(slot.request, slot.index, proof) await marketplace.fillSlot(slot.request, slot.index, proof)
await waitUntilCancelled(request) await waitUntilCancelled(request)
await marketplace.freeSlot(slotId(slot)) await marketplace.freeSlot(slotId(slot))
const expectedPartialPayout = (expiresAt - filledAt) * request.ask.reward
const endBalance = await token.balanceOf(host.address) const endBalance = await token.balanceOf(host.address)
const expectedPartialPayout =
(request.expiry - fillTimestamp) * request.ask.reward
expect(endBalance - ACCOUNT_STARTING_BALANCE).to.be.equal( expect(endBalance - ACCOUNT_STARTING_BALANCE).to.be.equal(
expectedPartialPayout expectedPartialPayout
) )
@ -617,14 +626,17 @@ describe("Marketplace", function () {
}) })
it("withdraws to the client for cancelled requests lowered by hosts payout", async function () { it("withdraws to the client for cancelled requests lowered by hosts payout", async function () {
const fillTimestamp = // Lets advance the time more into the expiry window
(await currentTime()) + const filledAt = (await currentTime()) + Math.floor(request.expiry / 3)
Math.floor((request.expiry - (await currentTime())) / 2) const expiresAt = (
await advanceTimeToForNextBlock(fillTimestamp) await marketplace.requestExpiry(requestId(request))
).toNumber()
await advanceTimeToForNextBlock(filledAt)
await marketplace.fillSlot(slot.request, slot.index, proof) await marketplace.fillSlot(slot.request, slot.index, proof)
await waitUntilCancelled(request) await waitUntilCancelled(request)
const expectedPartialHostPayout = const expectedPartialHostPayout =
(request.expiry - fillTimestamp) * request.ask.reward (expiresAt - filledAt) * request.ask.reward
switchAccount(client) switchAccount(client)
await marketplace.withdrawFunds(slot.request) await marketplace.withdrawFunds(slot.request)

View File

@ -1,6 +1,5 @@
const { ethers } = require("hardhat") const { ethers } = require("hardhat")
const { hours } = require("./time") const { hours } = require("./time")
const { currentTime } = require("./evm")
const { hexlify, randomBytes } = ethers.utils const { hexlify, randomBytes } = ethers.utils
const exampleConfiguration = () => ({ const exampleConfiguration = () => ({
@ -19,7 +18,6 @@ const exampleConfiguration = () => ({
}) })
const exampleRequest = async () => { const exampleRequest = async () => {
const now = await currentTime()
return { return {
client: hexlify(randomBytes(20)), client: hexlify(randomBytes(20)),
ask: { ask: {
@ -35,7 +33,7 @@ const exampleRequest = async () => {
cid: "zb2rhheVmk3bLks5MgzTqyznLu1zqGH5jrfTA1eAZXrjx7Vob", cid: "zb2rhheVmk3bLks5MgzTqyznLu1zqGH5jrfTA1eAZXrjx7Vob",
merkleRoot: Array.from(randomBytes(32)), merkleRoot: Array.from(randomBytes(32)),
}, },
expiry: now + hours(1), expiry: hours(1),
nonce: hexlify(randomBytes(32)), nonce: hexlify(randomBytes(32)),
} }
} }

View File

@ -2,8 +2,16 @@ const { advanceTimeToForNextBlock, currentTime } = require("./evm")
const { slotId, requestId } = require("./ids") const { slotId, requestId } = require("./ids")
const { price } = require("./price") const { price } = require("./price")
/**
* @dev This will not advance the time right on the "expiry threshold" but will most probably "overshoot it"
* because "currentTime" most probably is not the time at which the request is created, but it is used
* in the next timestamp calculation with `now + expiry`.
* @param request
* @returns {Promise<void>}
*/
async function waitUntilCancelled(request) { async function waitUntilCancelled(request) {
await advanceTimeToForNextBlock(request.expiry + 1) // We do +1, because the expiry check in contract is done as `>` and not `>=`.
await advanceTimeToForNextBlock((await currentTime()) + request.expiry + 1)
} }
async function waitUntilStarted(contract, request, proof, token) { async function waitUntilStarted(contract, request, proof, token) {
@ -16,6 +24,7 @@ async function waitUntilStarted(contract, request, proof, token) {
async function waitUntilFinished(contract, requestId) { async function waitUntilFinished(contract, requestId) {
const end = (await contract.requestEnd(requestId)).toNumber() const end = (await contract.requestEnd(requestId)).toNumber()
// We do +1, because the end check in contract is done as `>` and not `>=`.
await advanceTimeToForNextBlock(end + 1) await advanceTimeToForNextBlock(end + 1)
} }

View File

@ -1,4 +1,5 @@
const { Assertion } = require("chai") const { Assertion } = require("chai")
const { currentTime } = require("./evm")
const RequestState = { const RequestState = {
New: 0, New: 0,
@ -46,4 +47,8 @@ const enableRequestAssertions = function () {
}) })
} }
module.exports = { RequestState, SlotState, enableRequestAssertions } module.exports = {
RequestState,
SlotState,
enableRequestAssertions,
}