[marketplace] remove `offer`, `select` and `startContract`

Contract is started when first proof is submitted.
This commit is contained in:
Mark Spanbroek 2022-06-13 12:17:37 +02:00 committed by markspanbroek
parent f3b969fd7c
commit 8d7b7aed1d
6 changed files with 32 additions and 338 deletions

View File

@ -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;

View File

@ -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
}
}

View File

@ -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"
)
})
})
})

View File

@ -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

View File

@ -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 }

View File

@ -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,
}