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-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-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
) public marketplaceInvariant {
Request storage request = requests [ requestId ] ;
require ( request . client != address ( 0 ) , " Unknown request " ) ;
require ( request . expiry > block . timestamp , " Request expired " ) ;
2022-07-20 08:44:22 +00:00
require ( slotIndex < request . ask . slots , " Invalid slot " ) ;
2022-08-03 09:54:29 +00:00
RequestContext storage context = requestContexts [ requestId ] ;
// TODO: in the case of repair, update below require condition by adding
// || context.state == RequestState.Started
require ( context . state == RequestState . New , " Invalid state " ) ;
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 ) ;
_expectProofs ( slotId , request . ask . proofProbability , request . ask . duration ) ;
_submitProof ( slotId , proof ) ;
slot . host = msg . sender ;
2022-08-17 05:26:44 +00:00
slot . requestId = 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-08-09 11:19:42 +00:00
_extendLockExpiry ( requestId , block . timestamp + request . ask . duration ) ;
2022-07-20 08:29:24 +00:00
emit RequestFulfilled ( requestId ) ;
}
2022-07-19 09:33:54 +00:00
}
2022-07-19 15:09:35 +00:00
function payoutSlot ( bytes32 requestId , uint256 slotIndex )
public
marketplaceInvariant
{
bytes32 slotId = keccak256 ( abi . encode ( requestId , slotIndex ) ) ;
require ( block . timestamp > proofEnd ( slotId ) , " Contract not ended " ) ;
Slot storage slot = slots [ slotId ] ;
require ( slot . host != address ( 0 ) , " Slot empty " ) ;
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 " ) ;
RequestContext storage context = requestContexts [ 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 ) ;
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-08-17 05:26:44 +00:00
RequestContext memory context = requestContexts [ requestId ] ;
2022-08-12 01:40:04 +00:00
return
context . state == RequestState . Cancelled ||
(
context . state == RequestState . New &&
block . timestamp > requests [ requestId ] . expiry
) ;
}
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-02-22 08:25:42 +00:00
function _request ( bytes32 id ) internal view returns ( Request storage ) {
return requests [ id ] ;
}
2022-08-22 06:16:45 +00:00
function _slot ( bytes32 slotId ) internal view returns ( Slot memory ) {
Slot memory slot = slots [ slotId ] ;
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-20 05:56:27 +00:00
uint256 end = _end ( slotId ) ;
2022-09-08 07:56:01 +00:00
if ( ! _slotAcceptsProofs ( slotId ) ) {
2022-09-20 05:56:27 +00:00
return end < block . timestamp ? end : block . timestamp - 1 ;
2022-09-08 07:56:01 +00:00
}
2022-09-20 05:56:27 +00:00
return end ;
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
// TODO: add check for _isFinished
if ( _isCancelled ( requestId ) ) {
return RequestState . Cancelled ;
} 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-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-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-07-19 09:33:54 +00:00
event SlotFilled (
bytes32 indexed requestId ,
uint256 indexed slotIndex ,
bytes32 indexed 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-02-16 13:15:43 +00:00
struct MarketplaceFunds {
2022-02-16 09:50:00 +00:00
uint256 balance ;
uint256 received ;
uint256 sent ;
}
}