Offer storage using Marketplace contract

This commit is contained in:
Mark Spanbroek 2022-02-16 14:38:19 +01:00 committed by markspanbroek
parent f9cc73d62f
commit b349b76ab7
3 changed files with 107 additions and 12 deletions

View File

@ -2,19 +2,19 @@
pragma solidity ^0.8.0; pragma solidity ^0.8.0;
import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "./Collateral.sol";
contract Marketplace { contract Marketplace is Collateral {
IERC20 public immutable token; uint256 public immutable collateral;
MarketplaceFunds private funds; MarketplaceFunds private funds;
mapping(bytes32 => Request) private requests; mapping(bytes32 => Request) private requests;
mapping(bytes32 => Offer) private offers;
constructor(IERC20 _token) marketplaceInvariant { constructor(IERC20 _token, uint256 _collateral)
token = _token; Collateral(_token)
} marketplaceInvariant
{
function transferFrom(address sender, uint256 amount) private { collateral = _collateral;
address receiver = address(this);
require(token.transferFrom(sender, receiver, amount), "Transfer failed");
} }
function requestStorage(Request calldata request) function requestStorage(Request calldata request)
@ -31,6 +31,19 @@ contract Marketplace {
emit StorageRequested(id, request); 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 { struct Request {
uint256 duration; uint256 duration;
uint256 size; uint256 size;
@ -41,7 +54,14 @@ contract Marketplace {
bytes32 nonce; bytes32 nonce;
} }
struct Offer {
bytes32 requestId;
uint256 price;
uint256 expiry;
}
event StorageRequested(bytes32 id, Request request); event StorageRequested(bytes32 id, Request request);
event StorageOffered(bytes32 id, Offer offer);
modifier marketplaceInvariant() { modifier marketplaceInvariant() {
MarketplaceFunds memory oldFunds = funds; MarketplaceFunds memory oldFunds = funds;

View File

@ -1,10 +1,13 @@
const { ethers } = require("hardhat") const { ethers } = require("hardhat")
const { expect } = require("chai") const { expect } = require("chai")
const { exampleRequest } = require("./examples") const { exampleRequest, exampleOffer } = require("./examples")
const { now, hours } = require("./time")
const { keccak256, defaultAbiCoder } = ethers.utils const { keccak256, defaultAbiCoder } = ethers.utils
describe("Marketplace", function () { describe("Marketplace", function () {
const request = exampleRequest() const request = exampleRequest()
const offer = { ...exampleOffer(), requestId: requestId(request) }
const collateral = 100
let marketplace let marketplace
let token let token
@ -14,7 +17,7 @@ describe("Marketplace", function () {
const TestToken = await ethers.getContractFactory("TestToken") const TestToken = await ethers.getContractFactory("TestToken")
token = await TestToken.deploy() token = await TestToken.deploy()
const Marketplace = await ethers.getContractFactory("Marketplace") const Marketplace = await ethers.getContractFactory("Marketplace")
marketplace = await Marketplace.deploy(token.address) marketplace = await Marketplace.deploy(token.address, collateral)
accounts = await ethers.getSigners() accounts = await ethers.getSigners()
await token.mint(accounts[0].address, 1000) 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) { 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) { function requestToArray(request) {
return [ return [
request.duration, request.duration,
@ -81,3 +147,7 @@ function requestToArray(request) {
request.nonce, request.nonce,
] ]
} }
function offerToArray(offer) {
return [offer.requestId, offer.price, offer.expiry]
}

View File

@ -17,9 +17,14 @@ const exampleBid = () => ({
bidExpiry: now() + hours(1), bidExpiry: now() + hours(1),
}) })
const exampleOffer = () => ({
price: 42,
expiry: now() + hours(1),
})
const exampleLock = () => ({ const exampleLock = () => ({
id: hexlify(randomBytes(32)), id: hexlify(randomBytes(32)),
expiry: now() + hours(1), expiry: now() + hours(1),
}) })
module.exports = { exampleRequest, exampleBid, exampleLock } module.exports = { exampleRequest, exampleOffer, exampleBid, exampleLock }