diff --git a/contracts/Marketplace.sol b/contracts/Marketplace.sol index 1eba224..54143a9 100644 --- a/contracts/Marketplace.sol +++ b/contracts/Marketplace.sol @@ -3,16 +3,24 @@ pragma solidity ^0.8.0; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import "./Collateral.sol"; +import "./Proofs.sol"; -contract Marketplace is Collateral { +contract Marketplace is Collateral, Proofs { uint256 public immutable collateral; MarketplaceFunds private funds; mapping(bytes32 => Request) private requests; mapping(bytes32 => RequestState) private requestState; mapping(bytes32 => Offer) private offers; - constructor(IERC20 _token, uint256 _collateral) + constructor( + IERC20 _token, + uint256 _collateral, + uint256 _proofPeriod, + uint256 _proofTimeout, + uint8 _proofDowntime + ) Collateral(_token) + Proofs(_proofPeriod, _proofTimeout, _proofDowntime) marketplaceInvariant { collateral = _collateral; @@ -38,6 +46,26 @@ contract Marketplace is Collateral { emit StorageRequested(id, request.ask); } + function fulfillRequest(bytes32 requestId, bytes calldata proof) + public + marketplaceInvariant + { + RequestState storage state = requestState[requestId]; + require(!state.fulfilled, "Request already fulfilled"); + + Request storage request = requests[requestId]; + require(request.client != address(0), "Unknown request"); + require(request.expiry > block.timestamp, "Request expired"); + + require(balanceOf(msg.sender) >= collateral, "Insufficient collateral"); + _lock(msg.sender, requestId); + + _submitProof(requestId, proof); + + state.fulfilled = true; + emit RequestFulfilled(requestId); + } + function offerStorage(Offer calldata offer) public marketplaceInvariant { require(offer.host == msg.sender, "Invalid host address"); require(balanceOf(msg.sender) >= collateral, "Insufficient collateral"); @@ -129,6 +157,7 @@ contract Marketplace is Collateral { } struct RequestState { + bool fulfilled; bytes32 selectedOffer; } @@ -140,6 +169,7 @@ contract Marketplace is Collateral { } event StorageRequested(bytes32 requestId, Ask ask); + event RequestFulfilled(bytes32 indexed requestId); event StorageOffered(bytes32 offerId, Offer offer, bytes32 indexed requestId); event OfferSelected(bytes32 offerId, bytes32 indexed requestId); diff --git a/contracts/Storage.sol b/contracts/Storage.sol index b28f69b..36cf35a 100644 --- a/contracts/Storage.sol +++ b/contracts/Storage.sol @@ -5,7 +5,7 @@ import "./Marketplace.sol"; import "./Proofs.sol"; import "./Collateral.sol"; -contract Storage is Collateral, Marketplace, Proofs { +contract Storage is Collateral, Marketplace { uint256 public collateralAmount; uint256 public slashMisses; uint256 public slashPercentage; @@ -21,8 +21,13 @@ contract Storage is Collateral, Marketplace, Proofs { uint256 _slashMisses, uint256 _slashPercentage ) - Marketplace(token, _collateralAmount) - Proofs(_proofPeriod, _proofTimeout, _proofDowntime) + Marketplace( + token, + _collateralAmount, + _proofPeriod, + _proofTimeout, + _proofDowntime + ) { collateralAmount = _collateralAmount; slashMisses = _slashMisses; diff --git a/test/Marketplace.test.js b/test/Marketplace.test.js index 598e5f4..a39e251 100644 --- a/test/Marketplace.test.js +++ b/test/Marketplace.test.js @@ -1,11 +1,16 @@ const { ethers } = require("hardhat") +const { hexlify, randomBytes } = ethers.utils const { expect } = require("chai") const { exampleRequest, exampleOffer } = require("./examples") +const { snapshot, revert, ensureMinimumBlockHeight } = require("./evm") const { now, hours } = require("./time") const { requestId, offerId, offerToArray, askToArray } = require("./ids") describe("Marketplace", function () { const collateral = 100 + const proofPeriod = 30 * 60 + const proofTimeout = 5 + const proofDowntime = 64 let marketplace let token @@ -13,6 +18,8 @@ describe("Marketplace", function () { let request, offer beforeEach(async function () { + await snapshot() + await ensureMinimumBlockHeight(256) ;[client, host1, host2, host3] = await ethers.getSigners() host = host1 @@ -23,7 +30,13 @@ describe("Marketplace", function () { } const Marketplace = await ethers.getContractFactory("Marketplace") - marketplace = await Marketplace.deploy(token.address, collateral) + marketplace = await Marketplace.deploy( + token.address, + collateral, + proofPeriod, + proofTimeout, + proofDowntime + ) request = exampleRequest() request.client = client.address @@ -33,6 +46,10 @@ describe("Marketplace", function () { offer.requestId = requestId(request) }) + afterEach(async function () { + await revert() + }) + function switchAccount(account) { token = token.connect(account) marketplace = marketplace.connect(account) @@ -75,6 +92,72 @@ describe("Marketplace", function () { }) }) + describe("fulfilling request", function () { + const proof = hexlify(randomBytes(42)) + + beforeEach(async function () { + switchAccount(client) + await token.approve(marketplace.address, request.ask.maxPrice) + await marketplace.requestStorage(request) + switchAccount(host) + await token.approve(marketplace.address, collateral) + await marketplace.deposit(collateral) + }) + + it("emits event when request is fulfilled", async function () { + await expect(marketplace.fulfillRequest(requestId(request), proof)) + .to.emit(marketplace, "RequestFulfilled") + .withArgs(requestId(request)) + }) + + it("locks collateral of host", async function () { + await marketplace.fulfillRequest(requestId(request), proof) + await expect(marketplace.withdraw()).to.be.revertedWith("Account locked") + }) + + it("is rejected when proof is incorrect", async function () { + let invalid = hexlify([]) + await expect( + marketplace.fulfillRequest(requestId(request), invalid) + ).to.be.revertedWith("Invalid proof") + }) + + it("is rejected when collateral is insufficient", async function () { + let insufficient = collateral - 1 + await marketplace.withdraw() + await token.approve(marketplace.address, insufficient) + await marketplace.deposit(insufficient) + await expect( + marketplace.fulfillRequest(requestId(request), proof) + ).to.be.revertedWith("Insufficient collateral") + }) + + it("is rejected when request already fulfilled", async function () { + await marketplace.fulfillRequest(requestId(request), proof) + await expect( + marketplace.fulfillRequest(requestId(request), proof) + ).to.be.revertedWith("Request already fulfilled") + }) + + it("is rejected when request is unknown", async function () { + let unknown = exampleRequest() + await expect( + marketplace.fulfillRequest(requestId(unknown), proof) + ).to.be.revertedWith("Unknown request") + }) + + it("is rejected when request is expired", async function () { + switchAccount(client) + let expired = { ...request, expiry: now() - hours(1) } + await token.approve(marketplace.address, request.ask.maxPrice) + await marketplace.requestStorage(expired) + switchAccount(host) + await expect( + marketplace.fulfillRequest(requestId(expired), proof) + ).to.be.revertedWith("Request expired") + }) + }) + describe("offering storage", function () { beforeEach(async function () { switchAccount(client)