From 85b212c7035179945cfa963af89ae6840dc55e10 Mon Sep 17 00:00:00 2001 From: Mark Spanbroek Date: Mon, 21 Feb 2022 11:31:37 +0100 Subject: [PATCH] Select a storage offer --- contracts/Marketplace.sol | 24 ++++++++++++ test/Marketplace.test.js | 80 +++++++++++++++++++++++++++++++++++++-- test/examples.js | 3 +- 3 files changed, 102 insertions(+), 5 deletions(-) diff --git a/contracts/Marketplace.sol b/contracts/Marketplace.sol index eb576d9..57b311e 100644 --- a/contracts/Marketplace.sol +++ b/contracts/Marketplace.sol @@ -8,6 +8,7 @@ contract Marketplace is Collateral { 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) @@ -45,6 +46,25 @@ contract Marketplace is Collateral { emit StorageOffered(id, offer); } + function selectOffer(bytes32 id) public marketplaceInvariant { + Offer storage offer = offers[id]; + require(offer.host != address(0), "Unknown offer"); + // solhint-disable-next-line not-rely-on-time + 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.offerSelected, "Offer already selected"); + state.offerSelected = true; + _createLock(id, offer.expiry); + _lock(offer.host, id); + _unlock(offer.requestId); + uint256 difference = request.maxPrice - offer.price; + funds.sent += difference; + funds.balance -= difference; + token.transfer(request.client, difference); + } + struct Request { address client; uint256 duration; @@ -57,6 +77,10 @@ contract Marketplace is Collateral { bytes32 nonce; } + struct RequestState { + bool offerSelected; + } + struct Offer { address host; bytes32 requestId; diff --git a/test/Marketplace.test.js b/test/Marketplace.test.js index 31a38c5..43929ef 100644 --- a/test/Marketplace.test.js +++ b/test/Marketplace.test.js @@ -9,16 +9,18 @@ describe("Marketplace", function () { let marketplace let token - let client, host + let client, host, host1, host2, host3 let request, offer beforeEach(async function () { - ;[client, host] = await ethers.getSigners() + ;[client, host1, host2, host3] = await ethers.getSigners() + host = host1 const TestToken = await ethers.getContractFactory("TestToken") token = await TestToken.deploy() - await token.mint(client.address, 1000) - await token.mint(host.address, 1000) + for (account of [client, host1, host2, host3]) { + await token.mint(account.address, 1000) + } const Marketplace = await ethers.getContractFactory("Marketplace") marketplace = await Marketplace.deploy(token.address, collateral) @@ -133,6 +135,76 @@ describe("Marketplace", function () { ) }) }) + + describe("selecting an offer", async function () { + beforeEach(async function () { + switchAccount(client) + await token.approve(marketplace.address, request.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("returns price difference to client", async function () { + let difference = request.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" + ) + }) + }) }) function requestId(request) { diff --git a/test/examples.js b/test/examples.js index 9335ba7..67bc81b 100644 --- a/test/examples.js +++ b/test/examples.js @@ -9,7 +9,7 @@ const exampleRequest = () => ({ contentHash: sha256("0xdeadbeef"), proofPeriod: 8, // 8 blocks ≈ 2 minutes proofTimeout: 4, // 4 blocks ≈ 1 minute - maxPrice: 42, + maxPrice: 84, expiry: now() + hours(1), nonce: hexlify(randomBytes(32)), }) @@ -20,6 +20,7 @@ const exampleBid = () => ({ }) const exampleOffer = () => ({ + requestId: hexlify(randomBytes(32)), host: hexlify(randomBytes(20)), price: 42, expiry: now() + hours(1),