From 64ce222e24dae14127b51c929135d03b5e4f117d Mon Sep 17 00:00:00 2001 From: Eric <5089238+emizzle@users.noreply.github.com> Date: Wed, 18 Sep 2024 19:34:43 +1000 Subject: [PATCH] feat(slot-reservations): Allow slots to be reserved Closes #175. Allows reservation of slots, without an implementation of the expanding window. - Add a function called `reserveSlot(address, SlotId)`, that allows three unique addresses per slot to be reserved, that returns bool if successful. - Use `mapping(SlotId => EnumerableSet.AddressSet)` - Return false if the address could not be added to the set (if `EnumerableSet.add` returns false) - Add `canReserveSlot(address, SlotId)` - Return `true` if set of reservations is less than 3 and the set doesn't already contain the address - Return `true` otherwise (for now, later add in logic for checking the address is inside the expanding window) - Call `canReserveSlot` from `reserveSlot` as a `require` or invariant --- contracts/Marketplace.sol | 3 +- contracts/SlotReservations.sol | 29 ++++++++++++++ test/SlotReservations.test.js | 73 ++++++++++++++++++++++++++++++++++ test/examples.js | 3 ++ 4 files changed, 107 insertions(+), 1 deletion(-) create mode 100644 contracts/SlotReservations.sol create mode 100644 test/SlotReservations.test.js diff --git a/contracts/Marketplace.sol b/contracts/Marketplace.sol index 21aa466..f7a083a 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; diff --git a/contracts/SlotReservations.sol b/contracts/SlotReservations.sol new file mode 100644 index 0000000..d1bfd12 --- /dev/null +++ b/contracts/SlotReservations.sol @@ -0,0 +1,29 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.23; + +import "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; +import "./Requests.sol"; + +contract SlotReservations { + using EnumerableSet for EnumerableSet.AddressSet; + + mapping(SlotId => EnumerableSet.AddressSet) private _reservations; + + uint8 private constant _MAX_RESERVATIONS = 3; + + function reserveSlot(SlotId slotId, address host) public returns (bool) { + require(canReserveSlot(slotId, host), "Reservation not allowed"); + // returns false if set already contains address + return _reservations[slotId].add(host); + } + + function canReserveSlot( + SlotId slotId, + address host + ) public view returns (bool) { + return + // TODO: add in check for address inside of expanding window + (_reservations[slotId].length() < _MAX_RESERVATIONS) && + (!_reservations[slotId].contains(host)); + } +} diff --git a/test/SlotReservations.test.js b/test/SlotReservations.test.js new file mode 100644 index 0000000..d82f9f1 --- /dev/null +++ b/test/SlotReservations.test.js @@ -0,0 +1,73 @@ +const { expect } = require("chai") +const { ethers } = require("hardhat") +const { exampleRequest, exampleAddress } = require("./examples") +const { requestId, slotId } = require("./ids") + +describe("SlotReservations", function () { + let reservations + let provider, address1, address2, address3 + let request + let slot + + beforeEach(async function () { + let SlotReservations = await ethers.getContractFactory("SlotReservations") + reservations = await SlotReservations.deploy() + + provider = exampleAddress() + address1 = exampleAddress() + address2 = exampleAddress() + address3 = exampleAddress() + + request = await exampleRequest() + request.client = exampleAddress() + + slot = { + request: requestId(request), + index: request.ask.slots / 2, + } + }) + + it("allows a slot to be reserved", async function () { + let reserved = await reservations.callStatic.reserveSlot( + slotId(slot), + provider + ) + expect(reserved).to.be.true + }) + + it("reports a slot can be reserved", async function () { + expect(await reservations.canReserveSlot(slotId(slot), provider)).to.be.true + }) + + it("cannot reserve a slot more than once", async function () { + let id = slotId(slot) + await reservations.reserveSlot(id, provider) + await expect(reservations.reserveSlot(id, provider)).to.be.revertedWith( + "Reservation not allowed" + ) + }) + + it("reports a slot cannot be reserved if already reserved", async function () { + let id = slotId(slot) + await reservations.reserveSlot(id, provider) + expect(await reservations.canReserveSlot(id, provider)).to.be.false + }) + + it("cannot reserve a slot if reservations are at capacity", async function () { + let id = slotId(slot) + await reservations.reserveSlot(id, address1) + await reservations.reserveSlot(id, address2) + await reservations.reserveSlot(id, address3) + await expect(reservations.reserveSlot(id, provider)).to.be.revertedWith( + "Reservation not allowed" + ) + }) + + it("reports a slot cannot be reserved if reservations are at capacity", async function () { + let id = slotId(slot) + await reservations.reserveSlot(id, address1) + await reservations.reserveSlot(id, address2) + await reservations.reserveSlot(id, address3) + expect(await reservations.canReserveSlot(id, provider)).to.be.false + }) +}) diff --git a/test/examples.js b/test/examples.js index 06d8428..73900a3 100644 --- a/test/examples.js +++ b/test/examples.js @@ -51,9 +51,12 @@ const invalidProof = () => ({ c: { x: 0, y: 0 }, }) +const exampleAddress = () => hexlify(randomBytes(20)) + module.exports = { exampleConfiguration, exampleRequest, exampleProof, invalidProof, + exampleAddress, }