diff --git a/contracts/Configuration.sol b/contracts/Configuration.sol index 861a144..0f9e95d 100644 --- a/contracts/Configuration.sol +++ b/contracts/Configuration.sol @@ -6,6 +6,7 @@ import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; struct MarketplaceConfig { CollateralConfig collateral; ProofConfig proofs; + SlotReservationsConfig reservations; } struct CollateralConfig { @@ -29,3 +30,8 @@ struct ProofConfig { // blocks. Should be a prime number to ensure there are no cycles. uint8 downtimeProduct; } + +struct SlotReservationsConfig { + // Number of allowed reservations per slot + uint8 maxReservations; +} diff --git a/contracts/FuzzMarketplace.sol b/contracts/FuzzMarketplace.sol index efdd902..d7c20a3 100644 --- a/contracts/FuzzMarketplace.sol +++ b/contracts/FuzzMarketplace.sol @@ -10,7 +10,8 @@ contract FuzzMarketplace is Marketplace { Marketplace( MarketplaceConfig( CollateralConfig(10, 5, 3, 10), - ProofConfig(10, 5, 64, "", 67) + ProofConfig(10, 5, 64, "", 67), + SlotReservationsConfig(20) ), new TestToken(), new TestVerifier() diff --git a/contracts/Marketplace.sol b/contracts/Marketplace.sol index 21aa466..431e364 100644 --- a/contracts/Marketplace.sol +++ b/contracts/Marketplace.sol @@ -7,11 +7,12 @@ import "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; import "./Configuration.sol"; import "./Requests.sol"; import "./Proofs.sol"; +import "./SlotReservations.sol"; import "./StateRetrieval.sol"; import "./Endian.sol"; import "./Groth16.sol"; -contract Marketplace is Proofs, StateRetrieval, Endian { +contract Marketplace is SlotReservations, Proofs, StateRetrieval, Endian { using EnumerableSet for EnumerableSet.Bytes32Set; using Requests for Request; @@ -58,7 +59,10 @@ contract Marketplace is Proofs, StateRetrieval, Endian { MarketplaceConfig memory configuration, IERC20 token_, IGroth16Verifier verifier - ) Proofs(configuration.proofs, verifier) { + ) + SlotReservations(configuration.reservations) + Proofs(configuration.proofs, verifier) + { _token = token_; require( diff --git a/contracts/SlotReservations.sol b/contracts/SlotReservations.sol new file mode 100644 index 0000000..0327107 --- /dev/null +++ b/contracts/SlotReservations.sol @@ -0,0 +1,36 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.23; + +import "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; +import "./Requests.sol"; +import "./Configuration.sol"; + +contract SlotReservations { + using EnumerableSet for EnumerableSet.AddressSet; + + mapping(SlotId => EnumerableSet.AddressSet) internal _reservations; + SlotReservationsConfig private _config; + + constructor(SlotReservationsConfig memory config) { + _config = config; + } + + function reserveSlot(RequestId requestId, uint256 slotIndex) public { + require(canReserveSlot(requestId, slotIndex), "Reservation not allowed"); + + SlotId slotId = Requests.slotId(requestId, slotIndex); + _reservations[slotId].add(msg.sender); + } + + function canReserveSlot( + RequestId requestId, + uint256 slotIndex + ) public view returns (bool) { + address host = msg.sender; + SlotId slotId = Requests.slotId(requestId, slotIndex); + return + // TODO: add in check for address inside of expanding window + (_reservations[slotId].length() < _config.maxReservations) && + (!_reservations[slotId].contains(host)); + } +} diff --git a/contracts/TestSlotReservations.sol b/contracts/TestSlotReservations.sol new file mode 100644 index 0000000..31d19d6 --- /dev/null +++ b/contracts/TestSlotReservations.sol @@ -0,0 +1,19 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.23; + +import "./SlotReservations.sol"; + +contract TestSlotReservations is SlotReservations { + using EnumerableSet for EnumerableSet.AddressSet; + + // solhint-disable-next-line no-empty-blocks + constructor(SlotReservationsConfig memory config) SlotReservations(config) {} + + function contains(SlotId slotId, address host) public view returns (bool) { + return _reservations[slotId].contains(host); + } + + function length(SlotId slotId) public view returns (uint256) { + return _reservations[slotId].length(); + } +} diff --git a/deploy/marketplace.js b/deploy/marketplace.js index e4bc570..c4cc9db 100644 --- a/deploy/marketplace.js +++ b/deploy/marketplace.js @@ -16,6 +16,9 @@ const CONFIGURATION = { downtime: 64, downtimeProduct: 67 }, + reservations: { + maxReservations: 3 + } } async function mine256blocks({ network, ethers }) { diff --git a/test/SlotReservations.test.js b/test/SlotReservations.test.js new file mode 100644 index 0000000..055c294 --- /dev/null +++ b/test/SlotReservations.test.js @@ -0,0 +1,101 @@ +const { expect } = require("chai") +const { ethers } = require("hardhat") +const { exampleRequest, exampleConfiguration } = require("./examples") +const { requestId, slotId } = require("./ids") + +describe("SlotReservations", function () { + let reservations + let provider, address1, address2, address3 + let request + let reqId + let slot + let slotIndex + let id // can't use slotId because it'll shadow the function slotId + const config = exampleConfiguration() + + beforeEach(async function () { + let SlotReservations = await ethers.getContractFactory( + "TestSlotReservations" + ) + reservations = await SlotReservations.deploy(config.reservations) + ;[provider, address1, address2, address3] = await ethers.getSigners() + + request = await exampleRequest() + reqId = requestId(request) + slotIndex = request.ask.slots / 2 + slot = { + request: reqId, + index: slotIndex, + } + id = slotId(slot) + }) + + function switchAccount(account) { + reservations = reservations.connect(account) + } + + it("allows a slot to be reserved", async function () { + expect(reservations.reserveSlot(reqId, slotIndex)).to.not.be.reverted + }) + + it("contains the correct addresses after reservation", async function () { + await reservations.reserveSlot(reqId, slotIndex) + expect(await reservations.contains(id, provider.address)).to.be.true + + switchAccount(address1) + await reservations.reserveSlot(reqId, slotIndex) + expect(await reservations.contains(id, address1.address)).to.be.true + }) + + it("has the correct number of addresses after reservation", async function () { + await reservations.reserveSlot(reqId, slotIndex) + expect(await reservations.length(id)).to.equal(1) + + switchAccount(address1) + await reservations.reserveSlot(reqId, slotIndex) + expect(await reservations.length(id)).to.equal(2) + }) + + it("reports a slot can be reserved", async function () { + expect(await reservations.canReserveSlot(reqId, slotIndex)).to.be.true + }) + + it("cannot reserve a slot more than once", async function () { + await reservations.reserveSlot(reqId, slotIndex) + await expect(reservations.reserveSlot(reqId, slotIndex)).to.be.revertedWith( + "Reservation not allowed" + ) + expect(await reservations.length(id)).to.equal(1) + }) + + it("reports a slot cannot be reserved if already reserved", async function () { + await reservations.reserveSlot(reqId, slotIndex) + expect(await reservations.canReserveSlot(reqId, slotIndex)).to.be.false + }) + + it("cannot reserve a slot if reservations are at capacity", async function () { + switchAccount(address1) + await reservations.reserveSlot(reqId, slotIndex) + switchAccount(address2) + await reservations.reserveSlot(reqId, slotIndex) + switchAccount(address3) + await reservations.reserveSlot(reqId, slotIndex) + switchAccount(provider) + await expect(reservations.reserveSlot(reqId, slotIndex)).to.be.revertedWith( + "Reservation not allowed" + ) + expect(await reservations.length(id)).to.equal(3) + expect(await reservations.contains(id, provider.address)).to.be.false + }) + + it("reports a slot cannot be reserved if reservations are at capacity", async function () { + switchAccount(address1) + await reservations.reserveSlot(reqId, slotIndex) + switchAccount(address2) + await reservations.reserveSlot(reqId, slotIndex) + switchAccount(address3) + await reservations.reserveSlot(reqId, slotIndex) + switchAccount(provider) + expect(await reservations.canReserveSlot(reqId, slotIndex)).to.be.false + }) +}) diff --git a/test/examples.js b/test/examples.js index 06d8428..ac88a71 100644 --- a/test/examples.js +++ b/test/examples.js @@ -16,6 +16,9 @@ const exampleConfiguration = () => ({ zkeyHash: "", downtimeProduct: 67, }, + reservations: { + maxReservations: 3, + }, }) const exampleRequest = async () => {