2022-02-16 09:50:00 +00:00
// SPDX-License-Identifier: MIT
pragma solidity ^ 0 . 8 . 0 ;
import " @openzeppelin/contracts/token/ERC20/IERC20.sol " ;
2022-09-23 02:33:39 +00:00
import " @openzeppelin/contracts/utils/math/Math.sol " ;
2022-02-16 13:38:19 +00:00
import " ./Collateral.sol " ;
2022-06-13 08:40:18 +00:00
import " ./Proofs.sol " ;
2022-02-16 09:50:00 +00:00
2022-06-13 08:40:18 +00:00
contract Marketplace is Collateral , Proofs {
2022-02-16 13:38:19 +00:00
uint256 public immutable collateral ;
2022-02-16 13:15:43 +00:00
MarketplaceFunds private funds ;
2022-02-16 09:50:00 +00:00
mapping ( bytes32 => Request ) private requests ;
2022-08-03 09:54:29 +00:00
mapping ( bytes32 => RequestContext ) private requestContexts ;
2022-07-19 09:33:54 +00:00
mapping ( bytes32 => Slot ) private slots ;
2022-02-16 09:50:00 +00:00
2022-06-13 08:40:18 +00:00
constructor (
IERC20 _token ,
uint256 _collateral ,
uint256 _proofPeriod ,
uint256 _proofTimeout ,
uint8 _proofDowntime
)
2022-02-16 13:38:19 +00:00
Collateral ( _token )
2022-06-13 08:40:18 +00:00
Proofs ( _proofPeriod , _proofTimeout , _proofDowntime )
2022-02-16 13:38:19 +00:00
marketplaceInvariant
{
collateral = _collateral ;
2022-02-16 09:50:00 +00:00
}
2022-02-16 13:15:43 +00:00
function requestStorage ( Request calldata request )
public
marketplaceInvariant
{
2022-02-17 10:00:18 +00:00
require ( request . client == msg . sender , " Invalid client address " ) ;
2022-02-21 11:55:00 +00:00
bytes32 id = keccak256 ( abi . encode ( request ) ) ;
2022-02-17 10:09:35 +00:00
require ( requests [ id ] . client == address ( 0 ) , " Request already exists " ) ;
2022-02-21 11:55:00 +00:00
2022-02-16 09:50:00 +00:00
requests [ id ] = request ;
2022-09-29 10:07:55 +00:00
RequestContext storage context = _context ( id ) ;
// set contract end time to `duration` from now (time request was created)
context . endsAt = block . timestamp + request . ask . duration ;
_setProofEnd ( id , context . endsAt ) ;
2022-02-21 11:55:00 +00:00
2022-02-17 11:31:37 +00:00
_createLock ( id , request . expiry ) ;
2022-02-21 11:55:00 +00:00
2022-07-20 09:07:20 +00:00
uint256 amount = price ( request ) ;
funds . received += amount ;
funds . balance += amount ;
transferFrom ( msg . sender , amount ) ;
2022-02-21 11:55:00 +00:00
2022-04-06 12:26:56 +00:00
emit StorageRequested ( id , request . ask ) ;
2022-02-16 09:50:00 +00:00
}
2022-07-19 09:33:54 +00:00
function fillSlot (
bytes32 requestId ,
uint256 slotIndex ,
bytes calldata proof
2022-09-21 09:13:12 +00:00
) public requestMustAcceptProofs ( requestId ) marketplaceInvariant {
2022-09-13 07:14:57 +00:00
Request storage request = _request ( requestId ) ;
2022-07-20 08:44:22 +00:00
require ( slotIndex < request . ask . slots , " Invalid slot " ) ;
2022-07-19 09:33:54 +00:00
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 ) ;
2022-09-29 10:07:55 +00:00
_expectProofs ( slotId , requestId , request . ask . proofProbability ) ;
2022-07-19 09:33:54 +00:00
_submitProof ( slotId , proof ) ;
slot . host = msg . sender ;
2022-08-17 05:26:44 +00:00
slot . requestId = requestId ;
2022-09-21 09:57:26 +00:00
RequestContext storage context = _context ( requestId ) ;
2022-08-03 09:54:29 +00:00
context . slotsFilled += 1 ;
2022-07-19 09:33:54 +00:00
emit SlotFilled ( requestId , slotIndex , slotId ) ;
2022-08-03 09:54:29 +00:00
if ( context . slotsFilled == request . ask . slots ) {
context . state = RequestState . Started ;
2022-09-21 09:38:42 +00:00
context . startedAt = block . timestamp ;
2022-09-21 09:57:26 +00:00
_extendLockExpiryTo ( requestId , context . endsAt ) ;
2022-07-20 08:29:24 +00:00
emit RequestFulfilled ( requestId ) ;
}
2022-07-19 09:33:54 +00:00
}
2022-09-16 05:00:54 +00:00
function _freeSlot (
bytes32 slotId
2022-09-21 09:13:12 +00:00
) internal slotMustAcceptProofs ( slotId ) marketplaceInvariant {
2022-09-16 05:00:54 +00:00
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 ) ;
2022-09-21 09:13:12 +00:00
Request storage request = _request ( requestId ) ;
2022-09-16 05:00:54 +00:00
uint256 slotsLost = request . ask . slots - context . slotsFilled ;
2022-09-21 09:13:12 +00:00
if ( slotsLost > request . ask . maxSlotLoss &&
context . state == RequestState . Started ) {
2022-09-16 05:00:54 +00:00
context . state = RequestState . Failed ;
2022-09-29 10:07:55 +00:00
_setProofEnd ( requestId , block . timestamp - 1 ) ;
2022-09-23 02:33:39 +00:00
context . endsAt = block . timestamp - 1 ;
2022-09-16 05:00:54 +00:00
emit RequestFailed ( requestId ) ;
// TODO: burn all remaining slot collateral (note: slot collateral not
// yet implemented)
// TODO: send client remaining funds
}
}
2022-07-19 15:09:35 +00:00
function payoutSlot ( bytes32 requestId , uint256 slotIndex )
public
marketplaceInvariant
{
2022-09-21 09:38:42 +00:00
require ( _isFinished ( requestId ) , " Contract not ended " ) ;
2022-09-21 09:57:26 +00:00
RequestContext storage context = _context ( requestId ) ;
context . state = RequestState . Finished ;
2022-07-19 15:09:35 +00:00
bytes32 slotId = keccak256 ( abi . encode ( requestId , slotIndex ) ) ;
2022-09-21 09:38:42 +00:00
Slot storage slot = _slot ( slotId ) ;
2022-07-19 15:09:35 +00:00
require ( ! slot . hostPaid , " Already paid " ) ;
2022-07-20 09:07:20 +00:00
uint256 amount = pricePerSlot ( requests [ requestId ] ) ;
2022-07-19 15:09:35 +00:00
funds . sent += amount ;
funds . balance -= amount ;
slot . hostPaid = true ;
require ( token . transfer ( slot . host , amount ) , " Payment failed " ) ;
}
2022-08-17 05:26:04 +00:00
/// @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
2022-08-04 02:14:36 +00:00
function withdrawFunds ( bytes32 requestId ) public marketplaceInvariant {
2022-09-07 05:25:01 +00:00
Request storage request = requests [ requestId ] ;
2022-08-04 02:14:36 +00:00
require ( block . timestamp > request . expiry , " Request not yet timed out " ) ;
require ( request . client == msg . sender , " Invalid client address " ) ;
2022-09-21 09:57:26 +00:00
RequestContext storage context = _context ( requestId ) ;
2022-08-04 02:14:36 +00:00
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 ) ;
2022-09-07 05:25:01 +00:00
// 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 " ) ;
2022-08-04 02:14:36 +00:00
}
2022-08-17 05:26:44 +00:00
/// @notice Return true if the request state is RequestState.Cancelled or if the request expiry time has elapsed and the request was never started.
2022-08-17 05:26:04 +00:00
/// @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
2022-08-22 06:16:45 +00:00
function _isCancelled ( bytes32 requestId ) internal view returns ( bool ) {
2022-09-21 09:13:12 +00:00
RequestContext storage context = _context ( requestId ) ;
2022-08-12 01:40:04 +00:00
return
context . state == RequestState . Cancelled ||
(
context . state == RequestState . New &&
2022-09-21 09:13:12 +00:00
block . timestamp > _request ( requestId ) . expiry
2022-08-12 01:40:04 +00:00
) ;
}
2022-09-21 09:38:42 +00:00
/// @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 ) {
2022-09-21 09:57:26 +00:00
RequestContext memory context = _context ( requestId ) ;
2022-09-21 09:38:42 +00:00
return
context . state == RequestState . Finished ||
(
context . state == RequestState . Started &&
2022-09-21 09:57:26 +00:00
block . timestamp > context . endsAt
2022-09-21 09:38:42 +00:00
) ;
}
2022-08-22 06:16:45 +00:00
/// @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 ;
}
2022-08-17 05:26:44 +00:00
/// @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
2022-08-22 06:16:45 +00:00
function _isSlotCancelled ( bytes32 slotId ) internal view returns ( bool ) {
bytes32 requestId = _getRequestIdForSlot ( slotId ) ;
return _isCancelled ( requestId ) ;
2022-08-17 05:26:44 +00:00
}
2022-07-20 08:10:22 +00:00
function _host ( bytes32 slotId ) internal view returns ( address ) {
return slots [ slotId ] . host ;
2022-02-21 10:31:37 +00:00
}
2022-09-21 09:57:26 +00:00
function _request ( bytes32 requestId ) internal view returns ( Request storage ) {
Request storage request = requests [ requestId ] ;
2022-09-13 07:14:57 +00:00
require ( request . client != address ( 0 ) , " Unknown request " ) ;
2022-09-13 07:13:11 +00:00
return request ;
2022-02-22 08:25:42 +00:00
}
2022-09-13 07:14:57 +00:00
function _slot ( bytes32 slotId ) internal view returns ( Slot storage ) {
Slot storage slot = slots [ slotId ] ;
2022-08-22 06:16:45 +00:00
require ( slot . host != address ( 0 ) , " Slot empty " ) ;
return slot ;
}
2022-09-08 07:56:01 +00:00
function _context ( bytes32 requestId ) internal view returns ( RequestContext storage ) {
return requestContexts [ requestId ] ;
}
2022-06-13 09:49:25 +00:00
function proofPeriod ( ) public view returns ( uint256 ) {
return _period ( ) ;
}
function proofTimeout ( ) public view returns ( uint256 ) {
return _timeout ( ) ;
}
2022-08-17 05:24:19 +00:00
function proofEnd ( bytes32 slotId ) public view returns ( uint256 ) {
2022-09-21 09:57:26 +00:00
Slot memory slot = _slot ( slotId ) ;
2022-09-29 10:07:55 +00:00
uint256 end = _end ( slot . requestId ) ;
2022-09-23 02:33:39 +00:00
if ( _slotAcceptsProofs ( slotId ) ) {
return end ;
} else {
2022-09-29 10:07:55 +00:00
return Math . min ( end , block . timestamp - 1 ) ;
2022-09-08 07:56:01 +00:00
}
2022-06-13 09:49:25 +00:00
}
2022-08-04 02:14:36 +00:00
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 ) ;
}
2022-07-20 09:07:20 +00:00
function price ( Request calldata request ) private pure returns ( uint256 ) {
2022-08-04 02:14:36 +00:00
return _price ( request . ask . slots , request . ask . duration , request . ask . reward ) ;
2022-07-20 09:07:20 +00:00
}
function pricePerSlot ( Request memory request ) private pure returns ( uint256 ) {
return request . ask . duration * request . ask . reward ;
}
2022-08-03 09:54:29 +00:00
function state ( bytes32 requestId ) public view returns ( RequestState ) {
2022-09-08 07:56:01 +00:00
if ( _isCancelled ( requestId ) ) {
return RequestState . Cancelled ;
2022-09-22 02:21:49 +00:00
} else if ( _isFinished ( requestId ) ) {
2022-09-21 09:57:26 +00:00
return RequestState . Finished ;
2022-09-08 07:56:01 +00:00
} 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 ) ;
2022-08-03 09:54:29 +00:00
}
2022-02-16 09:50:00 +00:00
struct Request {
2022-02-17 10:00:18 +00:00
address client ;
2022-04-06 12:26:56 +00:00
Ask ask ;
Content content ;
uint256 expiry ; // time at which this request expires
bytes32 nonce ; // random nonce to differentiate between similar requests
}
struct Ask {
2022-08-02 09:41:49 +00:00
uint64 slots ; // the number of requested slots
uint256 slotSize ; // amount of storage per slot (in number of bytes)
2022-04-06 12:26:56 +00:00
uint256 duration ; // how long content should be stored (in seconds)
uint256 proofProbability ; // how often storage proofs are required
2022-07-20 09:07:20 +00:00
uint256 reward ; // amount of tokens paid per second per slot to hosts
2022-08-24 05:30:55 +00:00
uint64 maxSlotLoss ; // Max slots that can be lost without data considered to be lost
2022-04-06 12:26:56 +00:00
}
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 {
2022-04-07 13:44:56 +00:00
bytes u ; // parameters u_1..u_s
bytes publicKey ; // public key
bytes name ; // random name
2022-02-16 09:50:00 +00:00
}
2022-08-03 09:54:29 +00:00
enum RequestState {
New , // [default] waiting to fill slots
Started , // all slots filled, accepting regular proofs
Cancelled , // not enough slots filled before expiry
2022-08-04 02:14:36 +00:00
Finished , // successfully completed
Failed // too many nodes have failed to provide proofs, data lost
2022-08-03 09:54:29 +00:00
}
struct RequestContext {
2022-07-20 08:29:24 +00:00
uint256 slotsFilled ;
2022-08-03 09:54:29 +00:00
RequestState state ;
2022-09-21 09:38:42 +00:00
uint256 startedAt ;
2022-09-21 09:57:26 +00:00
uint256 endsAt ;
2022-07-20 08:29:24 +00:00
}
2022-07-19 09:33:54 +00:00
struct Slot {
address host ;
2022-07-19 15:09:35 +00:00
bool hostPaid ;
2022-08-17 05:26:44 +00:00
bytes32 requestId ;
2022-07-19 09:33:54 +00:00
}
2022-04-06 12:26:56 +00:00
event StorageRequested ( bytes32 requestId , Ask ask ) ;
2022-06-13 08:40:18 +00:00
event RequestFulfilled ( bytes32 indexed requestId ) ;
2022-08-24 05:30:55 +00:00
event RequestFailed ( bytes32 indexed requestId ) ;
2022-07-19 09:33:54 +00:00
event SlotFilled (
bytes32 indexed requestId ,
uint256 indexed slotIndex ,
2022-09-13 07:14:57 +00:00
bytes32 slotId
2022-07-19 09:33:54 +00:00
) ;
2022-09-13 07:14:57 +00:00
event SlotFreed ( bytes32 indexed requestId , bytes32 slotId ) ;
2022-09-07 10:07:04 +00:00
event RequestCancelled ( bytes32 indexed requestId ) ;
2022-02-16 09:50:00 +00:00
2022-02-16 13:15:43 +00:00
modifier marketplaceInvariant ( ) {
MarketplaceFunds memory oldFunds = funds ;
2022-02-16 09:50:00 +00:00
_ ;
2022-02-16 13:15:43 +00:00
assert ( funds . received >= oldFunds . received ) ;
assert ( funds . sent >= oldFunds . sent ) ;
assert ( funds . received == funds . balance + funds . sent ) ;
2022-02-16 09:50:00 +00:00
}
2022-09-08 07:56:01 +00:00
/// @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 " ) ;
_ ;
}
2022-09-21 09:13:12 +00:00
/// @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 " ) ;
_ ;
}
2022-02-16 13:15:43 +00:00
struct MarketplaceFunds {
2022-02-16 09:50:00 +00:00
uint256 balance ;
uint256 received ;
uint256 sent ;
}
}