From 33010bd20cfdc3d589be25782052796af580ca83 Mon Sep 17 00:00:00 2001 From: Eric <5089238+emizzle@users.noreply.github.com> Date: Thu, 3 Oct 2024 11:01:21 +1000 Subject: [PATCH] feat(slot-reservations): Allow slots to be reserved (#177) * 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 - Add `SlotReservations` configuration struct to the network-level config, with `maxReservations` --- contracts/Configuration.sol | 6 ++ contracts/FuzzMarketplace.sol | 3 +- contracts/Marketplace.sol | 8 ++- contracts/SlotReservations.sol | 36 ++++++++++ contracts/TestSlotReservations.sol | 19 ++++++ deploy/marketplace.js | 3 + test/SlotReservations.test.js | 101 +++++++++++++++++++++++++++++ test/examples.js | 3 + 8 files changed, 176 insertions(+), 3 deletions(-) create mode 100644 contracts/SlotReservations.sol create mode 100644 contracts/TestSlotReservations.sol create mode 100644 test/SlotReservations.test.js 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 () => {