dagger-contracts/contracts/Marketplace.sol

405 lines
13 KiB
Solidity

// SPDX-License-Identifier: MIT
pragma solidity 0.8.23;
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/utils/math/Math.sol";
import "@openzeppelin/contracts/utils/structs/EnumerableSet.sol";
import "./Configuration.sol";
import "./Requests.sol";
import "./Proofs.sol";
import "./StateRetrieval.sol";
import "./Endian.sol";
import "./Groth16.sol";
contract Marketplace is Proofs, StateRetrieval, Endian {
using EnumerableSet for EnumerableSet.Bytes32Set;
using Requests for Request;
IERC20 public immutable token;
MarketplaceConfig public config;
mapping(RequestId => Request) private _requests;
mapping(RequestId => RequestContext) private _requestContexts;
mapping(SlotId => Slot) internal _slots;
MarketplaceTotals internal _marketplaceTotals;
struct RequestContext {
RequestState state;
uint256 slotsFilled;
/// @notice Tracks how much funds should be returned when Request expires to the Request creator
/// @dev The sum is deducted every time a host fills a Slot by precalculated amount that he should receive if the Request expires
uint256 expiryFundsWithdraw;
uint256 startedAt;
uint256 endsAt;
}
struct Slot {
SlotState state;
RequestId requestId;
/// @notice Timestamp that signals when slot was filled
/// @dev Used for partial payouts when Requests expires and Hosts are paid out only the time they host the content.
uint256 filledAt;
uint256 slotIndex;
/// @notice Tracks the current amount of host's collateral that is to be payed out at the end of Slot's lifespan.
/// @dev When Slot is filled, the collateral is collected in amount of request.ask.collateral
/// @dev When Host is slashed for missing a proof the slashed amount is reflected in this variable
uint256 currentCollateral;
address host;
}
struct ActiveSlot {
Request request;
uint256 slotIndex;
}
constructor(
MarketplaceConfig memory configuration,
IERC20 token_,
IGroth16Verifier verifier
) Proofs(configuration.proofs, verifier) {
token = token_;
require(
configuration.collateral.repairRewardPercentage <= 100,
"Must be less than 100"
);
require(
configuration.collateral.slashPercentage <= 100,
"Must be less than 100"
);
require(
configuration.collateral.maxNumberOfSlashes *
configuration.collateral.slashPercentage <=
100,
"Maximum slashing exceeds 100%"
);
config = configuration;
}
function requestStorage(Request calldata request) public {
require(request.client == msg.sender, "Invalid client address");
RequestId id = request.id();
require(_requests[id].client == address(0), "Request already exists");
_requests[id] = request;
uint256 endsAt = block.timestamp + request.ask.duration;
require(endsAt > request.expiry, "Request end before expiry");
_requestContexts[id].endsAt = endsAt;
_addToMyRequests(request.client, id);
uint256 amount = request.price();
_requestContexts[id].expiryFundsWithdraw = amount;
_marketplaceTotals.received += amount;
_transferFrom(msg.sender, amount);
emit StorageRequested(id, request.ask, request.expiry);
}
function fillSlot(
RequestId requestId,
uint256 slotIndex,
Groth16Proof calldata proof
) public requestIsKnown(requestId) {
Request storage request = _requests[requestId];
require(slotIndex < request.ask.slots, "Invalid slot");
SlotId slotId = Requests.slotId(requestId, slotIndex);
Slot storage slot = _slots[slotId];
slot.requestId = requestId;
slot.slotIndex = slotIndex;
require(slotState(slotId) == SlotState.Free, "Slot is not free");
_startRequiringProofs(slotId, request.ask.proofProbability);
submitProof(slotId, proof);
slot.host = msg.sender;
slot.state = SlotState.Filled;
slot.filledAt = block.timestamp;
RequestContext storage context = _requestContexts[requestId];
context.slotsFilled += 1;
context.expiryFundsWithdraw -= _expiryPayoutAmount(
requestId,
block.timestamp
);
// Collect collateral
uint256 collateralAmount = request.ask.collateral;
_transferFrom(msg.sender, collateralAmount);
_marketplaceTotals.received += collateralAmount;
slot.currentCollateral = collateralAmount;
_addToMySlots(slot.host, slotId);
emit SlotFilled(requestId, slotIndex);
if (context.slotsFilled == request.ask.slots) {
context.state = RequestState.Started;
context.startedAt = block.timestamp;
emit RequestFulfilled(requestId);
}
}
function freeSlot(SlotId slotId) public slotIsNotFree(slotId) {
Slot storage slot = _slots[slotId];
require(slot.host == msg.sender, "Slot filled by other host");
SlotState state = slotState(slotId);
require(state != SlotState.Paid, "Already paid");
if (state == SlotState.Finished) {
_payoutSlot(slot.requestId, slotId);
} else if (state == SlotState.Cancelled) {
_payoutCancelledSlot(slot.requestId, slotId);
} else if (state == SlotState.Failed) {
_removeFromMySlots(msg.sender, slotId);
} else if (state == SlotState.Filled) {
_forciblyFreeSlot(slotId);
}
}
function _challengeToFieldElement(
bytes32 challenge
) internal pure returns (uint256) {
// use only 31 bytes of the challenge to ensure that it fits into the field
bytes32 truncated = bytes32(bytes31(challenge));
// convert from little endian to big endian
bytes32 bigEndian = _byteSwap(truncated);
// convert bytes to integer
return uint256(bigEndian);
}
function _merkleRootToFieldElement(
bytes32 merkleRoot
) internal pure returns (uint256) {
// convert from little endian to big endian
bytes32 bigEndian = _byteSwap(merkleRoot);
// convert bytes to integer
return uint256(bigEndian);
}
function submitProof(
SlotId id,
Groth16Proof calldata proof
) public requestIsKnown(_slots[id].requestId) {
Slot storage slot = _slots[id];
Request storage request = _requests[slot.requestId];
uint256[] memory pubSignals = new uint256[](3);
pubSignals[0] = _challengeToFieldElement(getChallenge(id));
pubSignals[1] = _merkleRootToFieldElement(request.content.merkleRoot);
pubSignals[2] = slot.slotIndex;
_proofReceived(id, proof, pubSignals);
}
function markProofAsMissing(SlotId slotId, Period period) public {
require(slotState(slotId) == SlotState.Filled, "Slot not accepting proofs");
_markProofAsMissing(slotId, period);
Slot storage slot = _slots[slotId];
Request storage request = _requests[slot.requestId];
if (missingProofs(slotId) % config.collateral.slashCriterion == 0) {
uint256 slashedAmount = (request.ask.collateral *
config.collateral.slashPercentage) / 100;
slot.currentCollateral -= slashedAmount;
if (
missingProofs(slotId) / config.collateral.slashCriterion >=
config.collateral.maxNumberOfSlashes
) {
// When the number of slashings is at or above the allowed amount,
// free the slot.
_forciblyFreeSlot(slotId);
}
}
}
function _forciblyFreeSlot(SlotId slotId) internal {
Slot storage slot = _slots[slotId];
RequestId requestId = slot.requestId;
RequestContext storage context = _requestContexts[requestId];
_removeFromMySlots(slot.host, slotId);
uint256 slotIndex = slot.slotIndex;
delete _slots[slotId];
context.slotsFilled -= 1;
emit SlotFreed(requestId, slotIndex);
_resetMissingProofs(slotId);
Request storage request = _requests[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: send client remaining funds
}
}
function _payoutSlot(
RequestId requestId,
SlotId slotId
) private requestIsKnown(requestId) {
RequestContext storage context = _requestContexts[requestId];
Request storage request = _requests[requestId];
context.state = RequestState.Finished;
_removeFromMyRequests(request.client, requestId);
Slot storage slot = _slots[slotId];
_removeFromMySlots(slot.host, slotId);
uint256 amount = _requests[requestId].pricePerSlot() +
slot.currentCollateral;
_marketplaceTotals.sent += amount;
slot.state = SlotState.Paid;
assert(token.transfer(slot.host, amount));
}
function _payoutCancelledSlot(
RequestId requestId,
SlotId slotId
) private requestIsKnown(requestId) {
Slot storage slot = _slots[slotId];
_removeFromMySlots(slot.host, slotId);
uint256 amount = _expiryPayoutAmount(requestId, slot.filledAt) +
slot.currentCollateral;
_marketplaceTotals.sent += amount;
slot.state = SlotState.Paid;
assert(token.transfer(slot.host, amount));
}
/// @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 {
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 = _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;
_removeFromMyRequests(request.client, requestId);
emit RequestCancelled(requestId);
uint256 amount = context.expiryFundsWithdraw;
_marketplaceTotals.sent += amount;
assert(token.transfer(msg.sender, amount));
}
function getActiveSlot(
SlotId slotId
) public view slotIsNotFree(slotId) returns (ActiveSlot memory) {
Slot storage slot = _slots[slotId];
ActiveSlot memory activeSlot;
activeSlot.request = _requests[slot.requestId];
activeSlot.slotIndex = slot.slotIndex;
return activeSlot;
}
modifier requestIsKnown(RequestId requestId) {
require(_requests[requestId].client != address(0), "Unknown request");
_;
}
function getRequest(
RequestId requestId
) public view requestIsKnown(requestId) returns (Request memory) {
return _requests[requestId];
}
modifier slotIsNotFree(SlotId slotId) {
require(_slots[slotId].state != SlotState.Free, "Slot is free");
_;
}
function requestEnd(RequestId requestId) public view returns (uint256) {
uint256 end = _requestContexts[requestId].endsAt;
RequestState state = requestState(requestId);
if (state == RequestState.New || state == RequestState.Started) {
return end;
} else {
return Math.min(end, block.timestamp - 1);
}
}
/// @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(
RequestId requestId,
uint256 startingTimestamp
) private view returns (uint256) {
Request storage request = _requests[requestId];
require(startingTimestamp < request.expiry, "Start not before expiry");
return (request.expiry - startingTimestamp) * request.ask.reward;
}
function getHost(SlotId slotId) public view returns (address) {
return _slots[slotId].host;
}
function requestState(
RequestId requestId
) public view requestIsKnown(requestId) returns (RequestState) {
RequestContext storage context = _requestContexts[requestId];
if (
context.state == RequestState.New &&
block.timestamp > _requests[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) public view override returns (SlotState) {
Slot storage slot = _slots[slotId];
if (RequestId.unwrap(slot.requestId) == 0) {
return SlotState.Free;
}
RequestState reqState = requestState(slot.requestId);
if (slot.state == SlotState.Paid) {
return SlotState.Paid;
}
if (reqState == RequestState.Cancelled) {
return SlotState.Cancelled;
}
if (reqState == RequestState.Finished) {
return SlotState.Finished;
}
if (reqState == RequestState.Failed) {
return SlotState.Failed;
}
return slot.state;
}
function _transferFrom(address sender, uint256 amount) internal {
address receiver = address(this);
require(token.transferFrom(sender, receiver, amount), "Transfer failed");
}
event StorageRequested(RequestId requestId, Ask ask, uint256 expiry);
event RequestFulfilled(RequestId indexed requestId);
event RequestFailed(RequestId indexed requestId);
event SlotFilled(RequestId indexed requestId, uint256 slotIndex);
event SlotFreed(RequestId indexed requestId, uint256 slotIndex);
event RequestCancelled(RequestId indexed requestId);
struct MarketplaceTotals {
uint256 received;
uint256 sent;
}
}