mirror of
https://github.com/codex-storage/codex-contracts-eth.git
synced 2025-01-22 01:19:55 +00:00
949a359626
Add cancelled check for slot state which checks the contract state and also the slot expiry time. This handles the case where the client may not have withdrawn funds yet (which sets the contract state to Cancelled). Add tests for contract state.
247 lines
7.4 KiB
Solidity
247 lines
7.4 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 marketplaceInvariant {
|
|
Request storage request = requests[requestId];
|
|
require(request.client != address(0), "Unknown request");
|
|
require(request.expiry > block.timestamp, "Request expired");
|
|
require(slotIndex < request.ask.slots, "Invalid slot");
|
|
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");
|
|
|
|
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;
|
|
context.slotsFilled += 1;
|
|
emit SlotFilled(requestId, slotIndex, slotId);
|
|
if (context.slotsFilled == request.ask.slots) {
|
|
context.state = RequestState.Started;
|
|
_extendLockExpiry(requestId, block.timestamp + request.ask.duration);
|
|
emit RequestFulfilled(requestId);
|
|
}
|
|
}
|
|
|
|
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");
|
|
uint256 amount = pricePerSlot(requests[requestId]);
|
|
funds.sent += amount;
|
|
funds.balance -= amount;
|
|
slot.hostPaid = true;
|
|
require(token.transfer(slot.host, amount), "Payment failed");
|
|
}
|
|
|
|
function withdrawFunds(bytes32 requestId) public marketplaceInvariant {
|
|
Request memory 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");
|
|
|
|
uint256 amount = _price(request);
|
|
funds.sent += amount;
|
|
funds.balance -= amount;
|
|
token.transfer(msg.sender, amount);
|
|
emit FundsWithdrawn(requestId);
|
|
|
|
// 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);
|
|
}
|
|
|
|
function isCancelled(bytes32 requestId) public view returns (bool) {
|
|
RequestContext storage context = requestContexts[requestId];
|
|
return
|
|
context.state == RequestState.Cancelled ||
|
|
(
|
|
context.state == RequestState.New &&
|
|
block.timestamp > requests[requestId].expiry
|
|
);
|
|
}
|
|
|
|
function _host(bytes32 slotId) internal view returns (address) {
|
|
return slots[slotId].host;
|
|
}
|
|
|
|
function _request(bytes32 id) internal view returns (Request storage) {
|
|
return requests[id];
|
|
}
|
|
|
|
function proofPeriod() public view returns (uint256) {
|
|
return _period();
|
|
}
|
|
|
|
function proofTimeout() public view returns (uint256) {
|
|
return _timeout();
|
|
}
|
|
|
|
function proofEnd(bytes32 slotId) public view returns (uint256) {
|
|
return _end(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 state(bytes32 requestId) public view returns (RequestState) {
|
|
return requestContexts[requestId].state;
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
struct Slot {
|
|
address host;
|
|
bool hostPaid;
|
|
}
|
|
|
|
event StorageRequested(bytes32 requestId, Ask ask);
|
|
event RequestFulfilled(bytes32 indexed requestId);
|
|
event SlotFilled(
|
|
bytes32 indexed requestId,
|
|
uint256 indexed slotIndex,
|
|
bytes32 indexed slotId
|
|
);
|
|
event RequestCancelled(bytes32 requestId);
|
|
event FundsWithdrawn(bytes32 requestId);
|
|
|
|
modifier marketplaceInvariant() {
|
|
MarketplaceFunds memory oldFunds = funds;
|
|
_;
|
|
assert(funds.received >= oldFunds.received);
|
|
assert(funds.sent >= oldFunds.sent);
|
|
assert(funds.received == funds.balance + funds.sent);
|
|
}
|
|
|
|
struct MarketplaceFunds {
|
|
uint256 balance;
|
|
uint256 received;
|
|
uint256 sent;
|
|
}
|
|
}
|