// SPDX-License-Identifier: MIT pragma solidity ^0.8.8; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import "@openzeppelin/contracts/utils/math/Math.sol"; import "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; import "./Requests.sol"; import "./Collateral.sol"; import "./Proofs.sol"; import "./StateRetrieval.sol"; contract Marketplace is Collateral, Proofs, StateRetrieval { using EnumerableSet for EnumerableSet.Bytes32Set; using Requests for Request; uint256 public immutable collateral; uint256 public immutable minCollateralThreshold; uint256 public immutable slashMisses; uint256 public immutable slashPercentage; MarketplaceFunds private funds; mapping(RequestId => Request) private requests; mapping(RequestId => RequestContext) private requestContexts; mapping(SlotId => Slot) private slots; constructor( IERC20 _token, uint256 _collateral, uint256 _minCollateralThreshold, uint256 _slashMisses, uint256 _slashPercentage, uint256 _proofPeriod, uint256 _proofTimeout, uint8 _proofDowntime ) Collateral(_token) Proofs(_proofPeriod, _proofTimeout, _proofDowntime) marketplaceInvariant { collateral = _collateral; minCollateralThreshold = _minCollateralThreshold; slashMisses = _slashMisses; slashPercentage = _slashPercentage; } function isWithdrawAllowed() internal view override returns (bool) { return !hasSlots(msg.sender); } function requestStorage( Request calldata request ) public marketplaceInvariant { require(request.client == msg.sender, "Invalid client address"); RequestId id = request.id(); require(requests[id].client == address(0), "Request already exists"); requests[id] = request; RequestContext storage context = _context(id); // set contract end time to `duration` from now (time request was created) context.endsAt = block.timestamp + request.ask.duration; addToMyRequests(request.client, id); uint256 amount = price(request); funds.received += amount; funds.balance += amount; transferFrom(msg.sender, amount); emit StorageRequested(id, request.ask); } function fillSlot( RequestId requestId, uint256 slotIndex, bytes calldata proof ) public requestMustAcceptProofs(requestId) marketplaceInvariant { Request storage request = _request(requestId); require(slotIndex < request.ask.slots, "Invalid slot"); SlotId slotId = Requests.slotId(requestId, slotIndex); Slot storage slot = slots[slotId]; slot.requestId = requestId; require(slotState(slotId) == SlotState.Free, "Slot already filled"); require(balanceOf(msg.sender) >= collateral, "Insufficient collateral"); _startRequiringProofs(slotId, request.ask.proofProbability); submitProof(slotId, proof); slot.host = msg.sender; slot.state = SlotState.Filled; RequestContext storage context = _context(requestId); context.slotsFilled += 1; addToMySlots(slot.host, slotId); emit SlotFilled(requestId, slotIndex, slotId); if (context.slotsFilled == request.ask.slots) { context.state = RequestState.Started; context.startedAt = block.timestamp; emit RequestFulfilled(requestId); } } function freeSlot(SlotId slotId) public { Slot storage slot = _slot(slotId); require(slot.host == msg.sender, "Slot filled by other host"); RequestState state = requestState(slot.requestId); if (state == RequestState.Finished || state == RequestState.Cancelled) { payoutSlot(slot.requestId, slotId); } else if (state == RequestState.Failed) { removeFromMySlots(msg.sender, slotId); } else { _forciblyFreeSlot(slotId); } } function markProofAsMissing( SlotId slotId, Period period ) public slotMustAcceptProofs(slotId) { _markProofAsMissing(slotId, period); address host = getHost(slotId); if (missingProofs(slotId) % slashMisses == 0) { _slash(host, slashPercentage); if (balanceOf(host) < minCollateralThreshold) { // When the collateral drops below the minimum threshold, the slot // needs to be freed so that there is enough remaining collateral to be // distributed for repairs and rewards (with any leftover to be burnt). _forciblyFreeSlot(slotId); } } } function _forciblyFreeSlot( SlotId slotId ) internal slotMustAcceptProofs(slotId) marketplaceInvariant // TODO: restrict senders that can call this function { Slot storage slot = _slot(slotId); RequestId 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. removeFromMySlots(slot.host, slotId); slot.state = SlotState.Free; slot.host = address(0); slot.requestId = RequestId.wrap(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; context.endsAt = block.timestamp - 1; emit RequestFailed(requestId); // TODO: burn all remaining slot collateral (note: slot collateral not // yet implemented) // TODO: send client remaining funds } } function payoutSlot( RequestId requestId, SlotId slotId ) private marketplaceInvariant { RequestState state = requestState(requestId); require( state == RequestState.Finished || state == RequestState.Cancelled, "Contract not ended" ); RequestContext storage context = _context(requestId); Request storage request = _request(requestId); context.state = RequestState.Finished; removeFromMyRequests(request.client, requestId); Slot storage slot = _slot(slotId); require(!slot.hostPaid, "Already paid"); removeFromMySlots(slot.host, slotId); 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(RequestId 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; removeFromMyRequests(request.client, requestId); 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 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( SlotId slotId ) internal view returns (RequestId) { Slot memory slot = _slot(slotId); require(RequestId.unwrap(slot.requestId) != 0, "Missing request id"); return slot.requestId; } function getHost(SlotId slotId) public view returns (address) { return slots[slotId].host; } function _request( RequestId requestId ) internal view returns (Request storage) { Request storage request = requests[requestId]; require(request.client != address(0), "Unknown request"); return request; } function getRequest( RequestId requestId ) public view returns (Request memory) { return _request(requestId); } function _slot(SlotId slotId) internal view returns (Slot storage) { Slot storage slot = slots[slotId]; require(slot.state == SlotState.Filled, "Slot empty"); return slot; } function _context( RequestId requestId ) internal view returns (RequestContext storage) { return requestContexts[requestId]; } function proofPeriod() public view returns (uint256) { return secondsPerPeriod; } function requestEnd(RequestId requestId) public view returns (uint256) { uint256 end = _context(requestId).endsAt; if (_requestAcceptsProofs(requestId)) { return end; } else { return Math.min(end, block.timestamp - 1); } } function isProofRequired(SlotId slotId) public view returns (bool) { if (slotState(slotId) != SlotState.Filled) { return false; } return _isProofRequired(slotId); } function willProofBeRequired(SlotId slotId) public view returns (bool) { if (slotState(slotId) != SlotState.Filled) { return false; } return _willProofBeRequired(slotId); } function getChallenge(SlotId slotId) public view returns (bytes32) { if (slotState(slotId) != SlotState.Filled) { return bytes32(0); } return _getChallenge(slotId); } function getPointer(SlotId slotId) public view returns (uint8) { return _getPointer(slotId); } 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 requestState( RequestId requestId ) public view returns (RequestState) { RequestContext storage context = _context(requestId); if ( context.state == RequestState.New && block.timestamp > _request(requestId).expiry ) { return RequestState.Cancelled; } else if ( context.state == RequestState.Started && block.timestamp > context.endsAt ) { return RequestState.Finished; } else { return context.state; } } function slotState(SlotId slotId) internal view override returns (SlotState) { Slot storage slot = slots[slotId]; RequestState reqState = requestState(slot.requestId); if ( reqState == RequestState.Finished || reqState == RequestState.Cancelled || reqState == RequestState.Failed ) { return SlotState.Finished; } else { return slot.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( RequestId requestId ) internal view returns (bool) { RequestState state = requestState(requestId); return state == RequestState.New || state == RequestState.Started; } struct RequestContext { RequestState state; uint256 slotsFilled; uint256 startedAt; uint256 endsAt; } struct Slot { SlotState state; address host; bool hostPaid; RequestId requestId; } event StorageRequested(RequestId requestId, Ask ask); event RequestFulfilled(RequestId indexed requestId); event RequestFailed(RequestId indexed requestId); event SlotFilled( RequestId indexed requestId, uint256 indexed slotIndex, SlotId slotId ); event SlotFreed(RequestId indexed requestId, SlotId slotId); event RequestCancelled(RequestId 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); } /// @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(SlotId slotId) { RequestId 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(RequestId requestId) { require(_requestAcceptsProofs(requestId), "Request not accepting proofs"); _; } struct MarketplaceFunds { uint256 balance; uint256 received; uint256 sent; } }