From 8d7b7aed1d81df53b72b139012ba94594edbcf34 Mon Sep 17 00:00:00 2001 From: Mark Spanbroek Date: Mon, 13 Jun 2022 12:17:37 +0200 Subject: [PATCH] [marketplace] remove `offer`, `select` and `startContract` Contract is started when first proof is submitted. --- contracts/Marketplace.sol | 68 +--------------- contracts/Storage.sol | 35 +++------ test/Marketplace.test.js | 159 +------------------------------------- test/Storage.test.js | 79 ++++--------------- test/examples.js | 14 +--- test/ids.js | 15 ---- 6 files changed, 32 insertions(+), 338 deletions(-) diff --git a/contracts/Marketplace.sol b/contracts/Marketplace.sol index 5497944..202c681 100644 --- a/contracts/Marketplace.sol +++ b/contracts/Marketplace.sol @@ -10,7 +10,6 @@ contract Marketplace is Collateral, Proofs { MarketplaceFunds private funds; mapping(bytes32 => Request) private requests; mapping(bytes32 => RequestState) private requestState; - mapping(bytes32 => Offer) private offers; constructor( IERC20 _token, @@ -51,7 +50,7 @@ contract Marketplace is Collateral, Proofs { marketplaceInvariant { RequestState storage state = requestState[requestId]; - require(!state.fulfilled, "Request already fulfilled"); + require(state.host == address(0), "Request already fulfilled"); Request storage request = requests[requestId]; require(request.client != address(0), "Unknown request"); @@ -67,67 +66,18 @@ contract Marketplace is Collateral, Proofs { ); _submitProof(requestId, proof); - state.fulfilled = true; + state.host = msg.sender; 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"); - - Request storage request = requests[offer.requestId]; - require(request.client != address(0), "Unknown request"); - require(request.expiry > block.timestamp, "Request expired"); - - require(offer.price <= request.ask.maxPrice, "Price too high"); - - bytes32 id = keccak256(abi.encode(offer)); - require(offers[id].host == address(0), "Offer already exists"); - - offers[id] = offer; - - _lock(msg.sender, offer.requestId); - - emit StorageOffered(id, offer, offer.requestId); - } - - function selectOffer(bytes32 id) public marketplaceInvariant { - Offer storage offer = offers[id]; - require(offer.host != address(0), "Unknown offer"); - require(offer.expiry > block.timestamp, "Offer expired"); - - Request storage request = requests[offer.requestId]; - require(request.client == msg.sender, "Only client can select offer"); - - RequestState storage state = requestState[offer.requestId]; - require(state.selectedOffer == bytes32(0), "Offer already selected"); - - state.selectedOffer = id; - - _createLock(id, offer.expiry); - _lock(offer.host, id); - _unlock(offer.requestId); - - uint256 difference = request.ask.maxPrice - offer.price; - funds.sent += difference; - funds.balance -= difference; - token.transfer(request.client, difference); - - emit OfferSelected(id, offer.requestId); + function _host(bytes32 requestId) internal view returns (address) { + return requestState[requestId].host; } function _request(bytes32 id) internal view returns (Request storage) { return requests[id]; } - function _offer(bytes32 id) internal view returns (Offer storage) { - return offers[id]; - } - - function _selectedOffer(bytes32 requestId) internal view returns (bytes32) { - return requestState[requestId].selectedOffer; - } - function proofPeriod() public view returns (uint256) { return _period(); } @@ -174,21 +124,11 @@ contract Marketplace is Collateral, Proofs { } struct RequestState { - bool fulfilled; - bytes32 selectedOffer; - } - - struct Offer { address host; - bytes32 requestId; - uint256 price; - uint256 expiry; } 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); modifier marketplaceInvariant() { MarketplaceFunds memory oldFunds = funds; diff --git a/contracts/Storage.sol b/contracts/Storage.sol index c96b39b..9f6fbd6 100644 --- a/contracts/Storage.sol +++ b/contracts/Storage.sol @@ -10,7 +10,7 @@ contract Storage is Collateral, Marketplace { uint256 public slashMisses; uint256 public slashPercentage; - mapping(bytes32 => ContractState) private contractState; + mapping(bytes32 => bool) private contractFinished; constructor( IERC20 token, @@ -38,25 +38,15 @@ contract Storage is Collateral, Marketplace { return _request(id); } - function getOffer(bytes32 id) public view returns (Offer memory) { - return _offer(id); - } - - function startContract(bytes32 id) public { - Offer storage offer = _offer(id); - require(msg.sender == offer.host, "Only host can call this function"); - require(_selectedOffer(offer.requestId) == id, "Offer was not selected"); - contractState[id] = ContractState.started; - Request storage request = _request(offer.requestId); - _expectProofs(id, request.ask.proofProbability, request.ask.duration); - } - function finishContract(bytes32 id) public { - require(contractState[id] == ContractState.started, "Contract not started"); + require(_host(id) != address(0), "Contract not started"); + require(!contractFinished[id], "Contract already finished"); require(block.timestamp > proofEnd(id), "Contract has not ended yet"); - contractState[id] = ContractState.finished; - Offer storage offer = _offer(id); - require(token.transfer(offer.host, offer.price), "Payment failed"); + contractFinished[id] = true; + require( + token.transfer(_host(id), _request(id).ask.maxPrice), + "Payment failed" + ); } function missingProofs(bytes32 contractId) public view returns (uint256) { @@ -86,14 +76,7 @@ contract Storage is Collateral, Marketplace { function markProofAsMissing(bytes32 contractId, uint256 period) public { _markProofAsMissing(contractId, period); if (_missed(contractId) % slashMisses == 0) { - Offer storage offer = _offer(contractId); - _slash(offer.host, slashPercentage); + _slash(_host(contractId), slashPercentage); } } - - enum ContractState { - none, - started, - finished - } } diff --git a/test/Marketplace.test.js b/test/Marketplace.test.js index f18cded..c774d75 100644 --- a/test/Marketplace.test.js +++ b/test/Marketplace.test.js @@ -1,10 +1,10 @@ const { ethers } = require("hardhat") const { hexlify, randomBytes } = ethers.utils const { expect } = require("chai") -const { exampleRequest, exampleOffer } = require("./examples") +const { exampleRequest } = require("./examples") const { snapshot, revert, ensureMinimumBlockHeight } = require("./evm") const { now, hours } = require("./time") -const { requestId, offerId, offerToArray, askToArray } = require("./ids") +const { requestId, askToArray } = require("./ids") describe("Marketplace", function () { const collateral = 100 @@ -15,7 +15,7 @@ describe("Marketplace", function () { let marketplace let token let client, host, host1, host2, host3 - let request, offer + let request beforeEach(async function () { await snapshot() @@ -40,10 +40,6 @@ describe("Marketplace", function () { request = exampleRequest() request.client = client.address - - offer = exampleOffer() - offer.host = host.address - offer.requestId = requestId(request) }) afterEach(async function () { @@ -162,153 +158,4 @@ describe("Marketplace", function () { ).to.be.revertedWith("Request expired") }) }) - - describe("offering storage", function () { - 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 storage is offered", async function () { - await expect(marketplace.offerStorage(offer)) - .to.emit(marketplace, "StorageOffered") - .withArgs(offerId(offer), offerToArray(offer), requestId(request)) - }) - - it("locks collateral of host", async function () { - await marketplace.offerStorage(offer) - await expect(marketplace.withdraw()).to.be.revertedWith("Account locked") - }) - - it("rejects offer with invalid host address", async function () { - let invalid = { ...offer, host: client.address } - await expect(marketplace.offerStorage(invalid)).to.be.revertedWith( - "Invalid host address" - ) - }) - - it("rejects offer for unknown request", async function () { - let unknown = exampleRequest() - let invalid = { ...offer, requestId: requestId(unknown) } - await expect(marketplace.offerStorage(invalid)).to.be.revertedWith( - "Unknown request" - ) - }) - - it("rejects offer for expired request", 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) - let invalid = { ...offer, requestId: requestId(expired) } - await expect(marketplace.offerStorage(invalid)).to.be.revertedWith( - "Request expired" - ) - }) - - it("rejects an offer that exceeds the maximum price", async function () { - let invalid = { ...offer, price: request.ask.maxPrice + 1 } - await expect(marketplace.offerStorage(invalid)).to.be.revertedWith( - "Price too high" - ) - }) - - it("rejects resubmission of offer", async function () { - await marketplace.offerStorage(offer) - await expect(marketplace.offerStorage(offer)).to.be.revertedWith( - "Offer already exists" - ) - }) - - it("rejects offer with insufficient collateral", async function () { - let insufficient = collateral - 1 - await marketplace.withdraw() - await token.approve(marketplace.address, insufficient) - await marketplace.deposit(insufficient) - await expect(marketplace.offerStorage(offer)).to.be.revertedWith( - "Insufficient collateral" - ) - }) - }) - - describe("selecting an offer", function () { - beforeEach(async function () { - switchAccount(client) - await token.approve(marketplace.address, request.ask.maxPrice) - await marketplace.requestStorage(request) - for (host of [host1, host2, host3]) { - switchAccount(host) - let hostOffer = { ...offer, host: host.address } - await token.approve(marketplace.address, collateral) - await marketplace.deposit(collateral) - await marketplace.offerStorage(hostOffer) - } - switchAccount(client) - }) - - it("emits event when offer is selected", async function () { - await expect(marketplace.selectOffer(offerId(offer))) - .to.emit(marketplace, "OfferSelected") - .withArgs(offerId(offer), requestId(request)) - }) - - it("returns price difference to client", async function () { - let difference = request.ask.maxPrice - offer.price - let before = await token.balanceOf(client.address) - await marketplace.selectOffer(offerId(offer)) - let after = await token.balanceOf(client.address) - expect(after - before).to.equal(difference) - }) - - it("unlocks collateral of hosts that weren't chosen", async function () { - await marketplace.selectOffer(offerId(offer)) - switchAccount(host2) - await expect(marketplace.withdraw()).not.to.be.reverted - switchAccount(host3) - await expect(marketplace.withdraw()).not.to.be.reverted - }) - - it("locks collateral of host that was chosen", async function () { - await marketplace.selectOffer(offerId(offer)) - switchAccount(host1) - await expect(marketplace.withdraw()).to.be.revertedWith("Account locked") - }) - - it("rejects selection of unknown offer", async function () { - let unknown = exampleOffer() - await expect( - marketplace.selectOffer(offerId(unknown)) - ).to.be.revertedWith("Unknown offer") - }) - - it("rejects selection of expired offer", async function () { - let expired = { ...offer, expiry: now() - hours(1) } - switchAccount(host1) - await marketplace.offerStorage(expired) - switchAccount(client) - await expect( - marketplace.selectOffer(offerId(expired)) - ).to.be.revertedWith("Offer expired") - }) - - it("rejects reselection of offer", async function () { - let secondOffer = { ...offer, host: host2.address } - await marketplace.selectOffer(offerId(offer)) - await expect( - marketplace.selectOffer(offerId(secondOffer)) - ).to.be.revertedWith("Offer already selected") - }) - - it("rejects selection by anyone other than the client", async function () { - switchAccount(host1) - await expect(marketplace.selectOffer(offerId(offer))).to.be.revertedWith( - "Only client can select offer" - ) - }) - }) }) diff --git a/test/Storage.test.js b/test/Storage.test.js index 447480d..9f56df8 100644 --- a/test/Storage.test.js +++ b/test/Storage.test.js @@ -1,15 +1,18 @@ const { expect } = require("chai") const { ethers, deployments } = require("hardhat") -const { exampleRequest, exampleOffer } = require("./examples") +const { hexlify, randomBytes } = ethers.utils +const { exampleRequest } = require("./examples") const { advanceTime, advanceTimeTo, currentTime } = require("./evm") -const { requestId, offerId } = require("./ids") +const { requestId } = require("./ids") const { periodic } = require("./time") describe("Storage", function () { + const proof = hexlify(randomBytes(42)) + let storage let token let client, host - let request, offer + let request let collateralAmount, slashMisses, slashPercentage let id @@ -34,10 +37,7 @@ describe("Storage", function () { request = exampleRequest() request.client = client.address - - offer = exampleOffer() - offer.host = host.address - offer.requestId = requestId(request) + id = requestId(request) switchAccount(client) await token.approve(storage.address, request.ask.maxPrice) @@ -45,10 +45,6 @@ describe("Storage", function () { switchAccount(host) await token.approve(storage.address, collateralAmount) await storage.deposit(collateralAmount) - await storage.offerStorage(offer) - switchAccount(client) - await storage.selectOffer(offerId(offer)) - id = offerId(offer) }) it("can retrieve storage requests", async function () { @@ -59,69 +55,26 @@ describe("Storage", function () { expect(retrieved.nonce).to.equal(request.nonce) }) - it("can retrieve storage offers", async function () { - const id = offerId(offer) - const retrieved = await storage.getOffer(id) - expect(retrieved.requestId).to.equal(offer.requestId) - expect(retrieved.host).to.equal(offer.host) - expect(retrieved.price).to.equal(offer.price) - expect(retrieved.expiry).to.equal(offer.expiry) - }) - - describe("starting the contract", function () { - it("starts requiring storage proofs", async function () { - switchAccount(host) - await storage.startContract(id) - expect(await storage.proofEnd(id)).to.be.gt(0) - }) - - it("can only be done by the host", async function () { - switchAccount(client) - await expect(storage.startContract(id)).to.be.revertedWith( - "Only host can call this function" - ) - }) - - it("can only be done once", async function () { - switchAccount(host) - await storage.startContract(id) - await expect(storage.startContract(id)).to.be.reverted - }) - - it("can only be done for a selected offer", async function () { - switchAccount(host) - const differentOffer = { ...offer, price: offer.price * 2 } - await storage.offerStorage(differentOffer) - await expect( - storage.startContract(offerId(differentOffer)) - ).to.be.revertedWith("Offer was not selected") - }) - }) - describe("finishing the contract", function () { - beforeEach(async function () { - switchAccount(host) - }) - async function waitUntilEnd() { const end = (await storage.proofEnd(id)).toNumber() await advanceTimeTo(end) } it("unlocks the host collateral", async function () { - await storage.startContract(id) + await storage.fulfillRequest(requestId(request), proof) await waitUntilEnd() await storage.finishContract(id) await expect(storage.withdraw()).not.to.be.reverted }) it("pays the host", async function () { - await storage.startContract(id) + await storage.fulfillRequest(requestId(request), proof) await waitUntilEnd() const startBalance = await token.balanceOf(host.address) await storage.finishContract(id) const endBalance = await token.balanceOf(host.address) - expect(endBalance - startBalance).to.equal(offer.price) + expect(endBalance - startBalance).to.equal(request.ask.maxPrice) }) it("is only allowed when the contract has started", async function () { @@ -131,24 +84,24 @@ describe("Storage", function () { }) it("is only allowed when end time has passed", async function () { - await storage.startContract(id) + await storage.fulfillRequest(requestId(request), proof) await expect(storage.finishContract(id)).to.be.revertedWith( "Contract has not ended yet" ) }) it("can only be done once", async function () { - await storage.startContract(id) + await storage.fulfillRequest(requestId(request), proof) await waitUntilEnd() await storage.finishContract(id) await expect(storage.finishContract(id)).to.be.reverted }) it("can not be restarted", async function () { - await storage.startContract(id) + await storage.fulfillRequest(requestId(request), proof) await waitUntilEnd() await storage.finishContract(id) - await expect(storage.startContract(id)).to.be.reverted + await expect(storage.fulfillRequest(id, proof)).to.be.reverted }) }) @@ -156,7 +109,6 @@ describe("Storage", function () { let period, periodOf, periodEnd beforeEach(async function () { - switchAccount(host) period = (await storage.proofPeriod()).toNumber() ;({ periodOf, periodEnd } = periodic(period)) }) @@ -174,7 +126,7 @@ describe("Storage", function () { } it("reduces collateral when too many proofs are missing", async function () { - await storage.connect(host).startContract(id) + await storage.fulfillRequest(requestId(request), proof) for (let i = 0; i < slashMisses; i++) { await waitUntilProofIsRequired() let missedPeriod = periodOf(await currentTime()) @@ -187,7 +139,6 @@ describe("Storage", function () { }) }) -// TODO: failure to start contract burns host and client // TODO: implement checking of actual proofs of storage, instead of dummy bool // TODO: allow other host to take over contract when too many missed proofs // TODO: small partial payouts when proofs are being submitted diff --git a/test/examples.js b/test/examples.js index 4da050c..22b23ac 100644 --- a/test/examples.js +++ b/test/examples.js @@ -27,21 +27,9 @@ const exampleRequest = () => ({ nonce: hexlify(randomBytes(32)), }) -const exampleBid = () => ({ - price: 42, - bidExpiry: now() + hours(1), -}) - -const exampleOffer = () => ({ - requestId: hexlify(randomBytes(32)), - host: hexlify(randomBytes(20)), - price: 42, - expiry: now() + hours(1), -}) - const exampleLock = () => ({ id: hexlify(randomBytes(32)), expiry: now() + hours(1), }) -module.exports = { exampleRequest, exampleOffer, exampleBid, exampleLock } +module.exports = { exampleRequest, exampleLock } diff --git a/test/ids.js b/test/ids.js index 053488f..56ba3ec 100644 --- a/test/ids.js +++ b/test/ids.js @@ -11,15 +11,6 @@ function requestId(request) { return keccak256(defaultAbiCoder.encode([Request], requestToArray(request))) } -function offerId(offer) { - return keccak256( - defaultAbiCoder.encode( - ["address", "bytes32", "uint256", "uint256"], - offerToArray(offer) - ) - ) -} - function askToArray(ask) { return [ask.size, ask.duration, ask.proofProbability, ask.maxPrice] } @@ -48,14 +39,8 @@ function requestToArray(request) { ] } -function offerToArray(offer) { - return [offer.host, offer.requestId, offer.price, offer.expiry] -} - module.exports = { requestId, - offerId, requestToArray, askToArray, - offerToArray, }