From b349b76ab7bb42a63ccc386b6f77c09bdf204e81 Mon Sep 17 00:00:00 2001 From: Mark Spanbroek Date: Wed, 16 Feb 2022 14:38:19 +0100 Subject: [PATCH] Offer storage using Marketplace contract --- contracts/Marketplace.sol | 38 +++++++++++++++----- test/Marketplace.test.js | 74 +++++++++++++++++++++++++++++++++++++-- test/examples.js | 7 +++- 3 files changed, 107 insertions(+), 12 deletions(-) diff --git a/contracts/Marketplace.sol b/contracts/Marketplace.sol index cb27b1c..8e3b777 100644 --- a/contracts/Marketplace.sol +++ b/contracts/Marketplace.sol @@ -2,19 +2,19 @@ pragma solidity ^0.8.0; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import "./Collateral.sol"; -contract Marketplace { - IERC20 public immutable token; +contract Marketplace is Collateral { + uint256 public immutable collateral; MarketplaceFunds private funds; mapping(bytes32 => Request) private requests; + mapping(bytes32 => Offer) private offers; - constructor(IERC20 _token) marketplaceInvariant { - token = _token; - } - - function transferFrom(address sender, uint256 amount) private { - address receiver = address(this); - require(token.transferFrom(sender, receiver, amount), "Transfer failed"); + constructor(IERC20 _token, uint256 _collateral) + Collateral(_token) + marketplaceInvariant + { + collateral = _collateral; } function requestStorage(Request calldata request) @@ -31,6 +31,19 @@ contract Marketplace { emit StorageRequested(id, request); } + function offerStorage(Offer calldata offer) public marketplaceInvariant { + bytes32 id = keccak256(abi.encode(offer)); + Request storage request = requests[offer.requestId]; + require(balanceOf(msg.sender) >= collateral, "Insufficient collateral"); + require(request.size != 0, "Unknown request"); + require(offers[id].expiry == 0, "Offer already exists"); + // solhint-disable-next-line not-rely-on-time + require(offer.expiry > block.timestamp, "Offer expired"); + require(offer.price <= request.maxPrice, "Price too high"); + offers[id] = offer; + emit StorageOffered(id, offer); + } + struct Request { uint256 duration; uint256 size; @@ -41,7 +54,14 @@ contract Marketplace { bytes32 nonce; } + struct Offer { + bytes32 requestId; + uint256 price; + uint256 expiry; + } + event StorageRequested(bytes32 id, Request request); + event StorageOffered(bytes32 id, Offer offer); modifier marketplaceInvariant() { MarketplaceFunds memory oldFunds = funds; diff --git a/test/Marketplace.test.js b/test/Marketplace.test.js index 174c1ec..6472b16 100644 --- a/test/Marketplace.test.js +++ b/test/Marketplace.test.js @@ -1,10 +1,13 @@ const { ethers } = require("hardhat") const { expect } = require("chai") -const { exampleRequest } = require("./examples") +const { exampleRequest, exampleOffer } = require("./examples") +const { now, hours } = require("./time") const { keccak256, defaultAbiCoder } = ethers.utils describe("Marketplace", function () { const request = exampleRequest() + const offer = { ...exampleOffer(), requestId: requestId(request) } + const collateral = 100 let marketplace let token @@ -14,7 +17,7 @@ describe("Marketplace", function () { const TestToken = await ethers.getContractFactory("TestToken") token = await TestToken.deploy() const Marketplace = await ethers.getContractFactory("Marketplace") - marketplace = await Marketplace.deploy(token.address) + marketplace = await Marketplace.deploy(token.address, collateral) accounts = await ethers.getSigners() await token.mint(accounts[0].address, 1000) }) @@ -51,6 +54,60 @@ describe("Marketplace", function () { ) }) }) + + describe("offering storage", function () { + beforeEach(async function () { + await token.approve(marketplace.address, request.maxPrice) + await marketplace.requestStorage(request) + 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)) + }) + + 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 an expired offer", async function () { + let expired = { ...offer, expiry: now() - hours(1) } + await expect(marketplace.offerStorage(expired)).to.be.revertedWith( + "Offer expired" + ) + }) + + it("rejects an offer that exceeds the maximum price", async function () { + let invalid = { ...offer, price: request.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" + ) + }) + }) }) function requestId(request) { @@ -70,6 +127,15 @@ function requestId(request) { ) } +function offerId(offer) { + return keccak256( + defaultAbiCoder.encode( + ["bytes32", "uint256", "uint256"], + offerToArray(offer) + ) + ) +} + function requestToArray(request) { return [ request.duration, @@ -81,3 +147,7 @@ function requestToArray(request) { request.nonce, ] } + +function offerToArray(offer) { + return [offer.requestId, offer.price, offer.expiry] +} diff --git a/test/examples.js b/test/examples.js index 05452c4..88ab615 100644 --- a/test/examples.js +++ b/test/examples.js @@ -17,9 +17,14 @@ const exampleBid = () => ({ bidExpiry: now() + hours(1), }) +const exampleOffer = () => ({ + price: 42, + expiry: now() + hours(1), +}) + const exampleLock = () => ({ id: hexlify(randomBytes(32)), expiry: now() + hours(1), }) -module.exports = { exampleRequest, exampleBid, exampleLock } +module.exports = { exampleRequest, exampleOffer, exampleBid, exampleLock }