Eric Mastro ad040cfee6 [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.
2022-10-25 12:38:19 +11:00

396 lines
14 KiB
Solidity

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "./Collateral.sol";
import "./Proofs.sol";
contract Marketplace is Collateral, Proofs {
uint256 public immutable collateral;
MarketplaceFunds private funds;
mapping(bytes32 => Request) private requests;
mapping(bytes32 => RequestContext) private requestContexts;
mapping(bytes32 => Slot) private slots;
constructor(
IERC20 _token,
uint256 _collateral,
uint256 _proofPeriod,
uint256 _proofTimeout,
uint8 _proofDowntime
)
Collateral(_token)
Proofs(_proofPeriod, _proofTimeout, _proofDowntime)
marketplaceInvariant
{
collateral = _collateral;
}
function requestStorage(Request calldata request)
public
marketplaceInvariant
{
require(request.client == msg.sender, "Invalid client address");
bytes32 id = keccak256(abi.encode(request));
require(requests[id].client == address(0), "Request already exists");
requests[id] = request;
_createLock(id, request.expiry);
uint256 amount = price(request);
funds.received += amount;
funds.balance += amount;
transferFrom(msg.sender, amount);
emit StorageRequested(id, request.ask);
}
function fillSlot(
bytes32 requestId,
uint256 slotIndex,
bytes calldata proof
) public requestMustAcceptProofs(requestId) marketplaceInvariant {
Request storage request = _request(requestId);
require(slotIndex < request.ask.slots, "Invalid slot");
bytes32 slotId = keccak256(abi.encode(requestId, slotIndex));
Slot storage slot = slots[slotId];
require(slot.host == address(0), "Slot already filled");
require(balanceOf(msg.sender) >= collateral, "Insufficient collateral");
_lock(msg.sender, requestId);
_expectProofs(slotId, requestId, request.ask.proofProbability, request.ask.duration);
_submitProof(slotId, proof);
slot.host = msg.sender;
slot.requestId = 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;
context.endsAt = block.timestamp + request.ask.duration;
_extendLockExpiryTo(requestId, context.endsAt);
_extendProofEndTo(slotId, context.endsAt);
emit RequestFulfilled(requestId);
}
}
function _freeSlot(
bytes32 slotId
) internal slotMustAcceptProofs(slotId) marketplaceInvariant {
Slot storage slot = _slot(slotId);
bytes32 requestId = slot.requestId;
RequestContext storage context = requestContexts[requestId];
// 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 storage request = _request(requestId);
uint256 slotsLost = request.ask.slots - context.slotsFilled;
if (slotsLost > request.ask.maxSlotLoss &&
context.state == RequestState.Started) {
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 payoutSlot(bytes32 requestId, uint256 slotIndex)
public
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");
uint256 amount = pricePerSlot(requests[requestId]);
funds.sent += amount;
funds.balance -= amount;
slot.hostPaid = true;
require(token.transfer(slot.host, amount), "Payment failed");
}
/// @notice Withdraws storage request funds back to the client that deposited them.
/// @dev Request must be expired, must be in RequestState.New, and the transaction must originate from the depositer address.
/// @param requestId the id of the request
function withdrawFunds(bytes32 requestId) public marketplaceInvariant {
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 = _context(requestId);
require(context.state == RequestState.New, "Invalid state");
// 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;
emit RequestCancelled(requestId);
// TODO: To be changed once we start paying out hosts for the time they
// fill a slot. The amount that we paid to hosts will then have to be
// deducted from the price.
uint256 amount = _price(request);
funds.sent += amount;
funds.balance -= amount;
require(token.transfer(msg.sender, amount), "Withdraw failed");
}
/// @notice Return true if the request state is RequestState.Cancelled or if the request expiry time has elapsed and the request was never started.
/// @dev Handles the case when a request may have been cancelled, but the client has not withdrawn its funds yet, and therefore the state has not yet been updated.
/// @param requestId the id of the request
/// @return true if request is cancelled
function _isCancelled(bytes32 requestId) internal view returns (bool) {
RequestContext storage context = _context(requestId);
return
context.state == RequestState.Cancelled ||
(
context.state == RequestState.New &&
block.timestamp > _request(requestId).expiry
);
}
/// @notice Return true if the request state is RequestState.Finished or if the request duration has elapsed and the request was started.
/// @dev Handles the case when a request may have been finished, but the state has not yet been updated by a transaction.
/// @param requestId the id of the request
/// @return true if request is finished
function _isFinished(bytes32 requestId) internal view returns (bool) {
RequestContext memory context = _context(requestId);
return
context.state == RequestState.Finished ||
(
context.state == RequestState.Started &&
block.timestamp > context.endsAt
);
}
/// @notice Return id of request that slot belongs to
/// @dev Returns requestId that is mapped to the slotId
/// @param slotId id of the slot
/// @return if of the request the slot belongs to
function _getRequestIdForSlot(bytes32 slotId) internal view returns (bytes32) {
Slot memory slot = _slot(slotId);
require(slot.requestId != 0, "Missing request id");
return slot.requestId;
}
/// @notice Return true if the request state the slot belongs to is RequestState.Cancelled or if the request expiry time has elapsed and the request was never started.
/// @dev Handles the case when a request may have been cancelled, but the client has not withdrawn its funds yet, and therefore the state has not yet been updated.
/// @param slotId the id of the slot
/// @return true if request is cancelled
function _isSlotCancelled(bytes32 slotId) internal view returns (bool) {
bytes32 requestId = _getRequestIdForSlot(slotId);
return _isCancelled(requestId);
}
function _host(bytes32 slotId) internal view returns (address) {
return slots[slotId].host;
}
function _request(bytes32 requestId) internal view returns (Request storage) {
Request storage request = requests[requestId];
require(request.client != address(0), "Unknown request");
return request;
}
function _slot(bytes32 slotId) internal view returns (Slot storage) {
Slot storage slot = slots[slotId];
require(slot.host != address(0), "Slot empty");
return slot;
}
function _context(bytes32 requestId) internal view returns (RequestContext storage) {
return requestContexts[requestId];
}
function proofPeriod() public view returns (uint256) {
return _period();
}
function proofTimeout() public view returns (uint256) {
return _timeout();
}
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;
}
return end;
}
function _price(
uint64 numSlots,
uint256 duration,
uint256 reward) internal pure returns (uint256) {
return numSlots * duration * reward;
}
function _price(Request memory request) internal pure returns (uint256) {
return _price(request.ask.slots, request.ask.duration, request.ask.reward);
}
function price(Request calldata request) private pure returns (uint256) {
return _price(request.ask.slots, request.ask.duration, request.ask.reward);
}
function pricePerSlot(Request memory request) private pure returns (uint256) {
return request.ask.duration * request.ask.reward;
}
function state(bytes32 requestId) public view returns (RequestState) {
// 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;
}
}
/// @notice returns true when the request 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 requestId id of the request for which to obtain state info
function _requestAcceptsProofs(bytes32 requestId) internal view returns (bool) {
RequestState s = state(requestId);
return s == RequestState.New || s == RequestState.Started;
}
/// @notice returns true when the request 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
function _slotAcceptsProofs(bytes32 slotId) internal view returns (bool) {
bytes32 requestId = _getRequestIdForSlot(slotId);
return _requestAcceptsProofs(requestId);
}
struct Request {
address client;
Ask ask;
Content content;
uint256 expiry; // time at which this request expires
bytes32 nonce; // random nonce to differentiate between similar requests
}
struct Ask {
uint64 slots; // the number of requested slots
uint256 slotSize; // amount of storage per slot (in number of bytes)
uint256 duration; // how long content should be stored (in seconds)
uint256 proofProbability; // how often storage proofs are required
uint256 reward; // amount of tokens paid per second per slot to hosts
uint64 maxSlotLoss; // Max slots that can be lost without data considered to be lost
}
struct Content {
string cid; // content id (if part of a larger set, the chunk cid)
Erasure erasure; // Erasure coding attributes
PoR por; // Proof of Retrievability parameters
}
struct Erasure {
uint64 totalChunks; // the total number of chunks in the larger data set
}
struct PoR {
bytes u; // parameters u_1..u_s
bytes publicKey; // public key
bytes name; // random name
}
enum RequestState {
New, // [default] waiting to fill slots
Started, // all slots filled, accepting regular proofs
Cancelled, // not enough slots filled before expiry
Finished, // successfully completed
Failed // too many nodes have failed to provide proofs, data lost
}
struct RequestContext {
uint256 slotsFilled;
RequestState state;
uint256 startedAt;
uint256 endsAt;
}
struct Slot {
address host;
bool hostPaid;
bytes32 requestId;
}
event StorageRequested(bytes32 requestId, Ask ask);
event RequestFulfilled(bytes32 indexed requestId);
event RequestFailed(bytes32 indexed requestId);
event SlotFilled(
bytes32 indexed requestId,
uint256 indexed slotIndex,
bytes32 slotId
);
event SlotFreed(bytes32 indexed requestId, bytes32 slotId);
event RequestCancelled(bytes32 indexed requestId);
modifier marketplaceInvariant() {
MarketplaceFunds memory oldFunds = funds;
_;
assert(funds.received >= oldFunds.received);
assert(funds.sent >= oldFunds.sent);
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
modifier slotMustAcceptProofs(bytes32 slotId) {
bytes32 requestId = _getRequestIdForSlot(slotId);
require(_requestAcceptsProofs(requestId), "Slot not accepting proofs");
_;
}
/// @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 requestId id of the request, for which to obtain state info
modifier requestMustAcceptProofs(bytes32 requestId) {
require(_requestAcceptsProofs(requestId), "Request not accepting proofs");
_;
}
struct MarketplaceFunds {
uint256 balance;
uint256 received;
uint256 sent;
}
}