diff --git a/contracts/Marketplace.sol b/contracts/Marketplace.sol new file mode 100644 index 0000000..3629151 --- /dev/null +++ b/contracts/Marketplace.sol @@ -0,0 +1,56 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +contract Marketplace { + IERC20 public immutable token; + Totals private totals; + mapping(bytes32 => Request) private requests; + + constructor(IERC20 _token) invariant { + token = _token; + } + + function transferFrom(address sender, uint256 amount) private { + address receiver = address(this); + require(token.transferFrom(sender, receiver, amount), "Transfer failed"); + } + + function requestStorage(Request calldata request) public invariant { + bytes32 id = keccak256(abi.encode(request)); + require(request.size > 0, "Invalid size"); + require(requests[id].size == 0, "Request already exists"); + requests[id] = request; + transferFrom(msg.sender, request.maxPrice); + totals.received += request.maxPrice; + totals.balance += request.maxPrice; + emit StorageRequested(id, request); + } + + struct Request { + uint256 duration; + uint256 size; + bytes32 contentHash; + uint256 proofPeriod; + uint256 proofTimeout; + uint256 maxPrice; + bytes32 nonce; + } + + event StorageRequested(bytes32 id, Request request); + + modifier invariant() { + Totals memory oldTotals = totals; + _; + assert(totals.received >= oldTotals.received); + assert(totals.sent >= oldTotals.sent); + assert(totals.received == totals.balance + totals.sent); + } + + struct Totals { + uint256 balance; + uint256 received; + uint256 sent; + } +} diff --git a/test/Marketplace.test.js b/test/Marketplace.test.js new file mode 100644 index 0000000..174c1ec --- /dev/null +++ b/test/Marketplace.test.js @@ -0,0 +1,83 @@ +const { ethers } = require("hardhat") +const { expect } = require("chai") +const { exampleRequest } = require("./examples") +const { keccak256, defaultAbiCoder } = ethers.utils + +describe("Marketplace", function () { + const request = exampleRequest() + + let marketplace + let token + let accounts + + beforeEach(async function () { + const TestToken = await ethers.getContractFactory("TestToken") + token = await TestToken.deploy() + const Marketplace = await ethers.getContractFactory("Marketplace") + marketplace = await Marketplace.deploy(token.address) + accounts = await ethers.getSigners() + await token.mint(accounts[0].address, 1000) + }) + + describe("requesting storage", function () { + it("emits event when storage is requested", async function () { + await token.approve(marketplace.address, request.maxPrice) + await expect(marketplace.requestStorage(request)) + .to.emit(marketplace, "StorageRequested") + .withArgs(requestId(request), requestToArray(request)) + }) + + it("rejects request with insufficient payment", async function () { + let insufficient = request.maxPrice - 1 + await token.approve(marketplace.address, insufficient) + await expect(marketplace.requestStorage(request)).to.be.revertedWith( + "ERC20: transfer amount exceeds allowance" + ) + }) + + it("rejects requests of size 0", async function () { + let invalid = { ...request, size: 0 } + await token.approve(marketplace.address, invalid.maxPrice) + await expect(marketplace.requestStorage(invalid)).to.be.revertedWith( + "Invalid size" + ) + }) + + it("rejects resubmission of request", async function () { + await token.approve(marketplace.address, request.maxPrice * 2) + await marketplace.requestStorage(request) + await expect(marketplace.requestStorage(request)).to.be.revertedWith( + "Request already exists" + ) + }) + }) +}) + +function requestId(request) { + return keccak256( + defaultAbiCoder.encode( + [ + "uint256", + "uint256", + "bytes32", + "uint256", + "uint256", + "uint256", + "bytes32", + ], + requestToArray(request) + ) + ) +} + +function requestToArray(request) { + return [ + request.duration, + request.size, + request.contentHash, + request.proofPeriod, + request.proofTimeout, + request.maxPrice, + request.nonce, + ] +} diff --git a/test/examples.js b/test/examples.js index 9ec4da6..05452c4 100644 --- a/test/examples.js +++ b/test/examples.js @@ -1,13 +1,15 @@ const { ethers } = require("hardhat") const { now, hours } = require("./time") +const { sha256, hexlify, randomBytes } = ethers.utils const exampleRequest = () => ({ duration: 150, // 150 blocks ≈ half an hour size: 1 * 1024 * 1024 * 1024, // 1 Gigabyte - contentHash: ethers.utils.sha256("0xdeadbeef"), + contentHash: sha256("0xdeadbeef"), proofPeriod: 8, // 8 blocks ≈ 2 minutes proofTimeout: 4, // 4 blocks ≈ 1 minute - nonce: ethers.utils.randomBytes(32), + maxPrice: 42, + nonce: hexlify(randomBytes(32)), }) const exampleBid = () => ({ @@ -16,7 +18,7 @@ const exampleBid = () => ({ }) const exampleLock = () => ({ - id: ethers.utils.randomBytes(32), + id: hexlify(randomBytes(32)), expiry: now() + hours(1), })