diff --git a/contracts/Configuration.sol b/contracts/Configuration.sol new file mode 100644 index 0000000..7e41ec0 --- /dev/null +++ b/contracts/Configuration.sol @@ -0,0 +1,22 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.8; + +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +struct MarketplaceConfig { + CollateralConfig collateral; + ProofConfig proofs; +} + +struct CollateralConfig { + uint256 initialAmount; // amount of collateral necessary to fill a slot + uint256 minimumAmount; // frees slot when collateral drops below this minimum + uint256 slashCriterion; // amount of proofs missed that lead to slashing + uint256 slashPercentage; // percentage of the collateral that is slashed +} + +struct ProofConfig { + uint256 period; // proofs requirements are calculated per period (in seconds) + uint256 timeout; // mark proofs as missing before the timeout (in seconds) + uint8 downtime; // ignore this much recent blocks for proof requirements +} diff --git a/contracts/Marketplace.sol b/contracts/Marketplace.sol index e88065a..4f5fa25 100644 --- a/contracts/Marketplace.sol +++ b/contracts/Marketplace.sol @@ -4,6 +4,7 @@ pragma solidity ^0.8.8; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import "@openzeppelin/contracts/utils/math/Math.sol"; import "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; +import "./Configuration.sol"; import "./Requests.sol"; import "./Collateral.sol"; import "./Proofs.sol"; @@ -13,10 +14,7 @@ contract Marketplace is Collateral, Proofs, StateRetrieval { using EnumerableSet for EnumerableSet.Bytes32Set; using Requests for Request; - uint256 public immutable collateral; - uint256 public immutable minCollateralThreshold; - uint256 public immutable slashMisses; - uint256 public immutable slashPercentage; + MarketplaceConfig public config; MarketplaceFunds private funds; mapping(RequestId => Request) private requests; @@ -24,23 +22,10 @@ contract Marketplace is Collateral, Proofs, StateRetrieval { mapping(SlotId => Slot) private slots; constructor( - IERC20 _token, - uint256 _collateral, - uint256 _minCollateralThreshold, - uint256 _slashMisses, - uint256 _slashPercentage, - uint256 _proofPeriod, - uint256 _proofTimeout, - uint8 _proofDowntime - ) - Collateral(_token) - Proofs(_proofPeriod, _proofTimeout, _proofDowntime) - marketplaceInvariant - { - collateral = _collateral; - minCollateralThreshold = _minCollateralThreshold; - slashMisses = _slashMisses; - slashPercentage = _slashPercentage; + IERC20 token, + MarketplaceConfig memory configuration + ) Collateral(token) Proofs(configuration.proofs) marketplaceInvariant { + config = configuration; } function isWithdrawAllowed() internal view override returns (bool) { @@ -82,7 +67,10 @@ contract Marketplace is Collateral, Proofs, StateRetrieval { require(slotState(slotId) == SlotState.Free, "Slot is not free"); - require(balanceOf(msg.sender) >= collateral, "Insufficient collateral"); + require( + balanceOf(msg.sender) >= config.collateral.initialAmount, + "Insufficient collateral" + ); _startRequiringProofs(slotId, request.ask.proofProbability); submitProof(slotId, proof); @@ -120,10 +108,10 @@ contract Marketplace is Collateral, Proofs, StateRetrieval { require(slotState(slotId) == SlotState.Filled, "Slot not accepting proofs"); _markProofAsMissing(slotId, period); address host = getHost(slotId); - if (missingProofs(slotId) % slashMisses == 0) { - _slash(host, slashPercentage); + if (missingProofs(slotId) % config.collateral.slashCriterion == 0) { + _slash(host, config.collateral.slashPercentage); - if (balanceOf(host) < minCollateralThreshold) { + if (balanceOf(host) < config.collateral.minimumAmount) { // When the collateral drops below the minimum threshold, the slot // needs to be freed so that there is enough remaining collateral to be // distributed for repairs and rewards (with any leftover to be burnt). diff --git a/contracts/Proofs.sol b/contracts/Proofs.sol index ffea334..cda0ef7 100644 --- a/contracts/Proofs.sol +++ b/contracts/Proofs.sol @@ -1,21 +1,16 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.8; +import "./Configuration.sol"; import "./Requests.sol"; import "./Periods.sol"; abstract contract Proofs is Periods { - uint256 public immutable proofTimeout; - uint8 private immutable downtime; + ProofConfig private config; - constructor( - uint256 __period, - uint256 __timeout, - uint8 __downtime - ) Periods(__period) { + constructor(ProofConfig memory _config) Periods(_config.period) { require(block.number > 256, "Insufficient block height"); - proofTimeout = __timeout; - downtime = __downtime; + config = _config; } mapping(SlotId => uint256) private slotStarts; @@ -26,10 +21,6 @@ abstract contract Proofs is Periods { function slotState(SlotId id) internal view virtual returns (SlotState); - function proofPeriod() public view returns (uint256) { - return secondsPerPeriod; - } - function missingProofs(SlotId slotId) public view returns (uint256) { return missed[slotId]; } @@ -79,7 +70,7 @@ abstract contract Proofs is Periods { } pointer = getPointer(id, period); bytes32 challenge = getChallenge(pointer); - uint256 probability = (probabilities[id] * (256 - downtime)) / 256; + uint256 probability = (probabilities[id] * (256 - config.downtime)) / 256; isRequired = uint256(challenge) % probability == 0; } @@ -90,7 +81,7 @@ abstract contract Proofs is Periods { bool isRequired; uint8 pointer; (isRequired, pointer) = _getProofRequirement(id, period); - return isRequired && pointer >= downtime; + return isRequired && pointer >= config.downtime; } function isProofRequired(SlotId id) public view returns (bool) { @@ -101,7 +92,7 @@ abstract contract Proofs is Periods { bool isRequired; uint8 pointer; (isRequired, pointer) = _getProofRequirement(id, blockPeriod()); - return isRequired && pointer < downtime; + return isRequired && pointer < config.downtime; } function submitProof(SlotId id, bytes calldata proof) public { @@ -112,9 +103,9 @@ abstract contract Proofs is Periods { } function _markProofAsMissing(SlotId id, Period missedPeriod) internal { - uint256 periodEnd = periodEnd(missedPeriod); - require(periodEnd < block.timestamp, "Period has not ended yet"); - require(block.timestamp < periodEnd + proofTimeout, "Validation timed out"); + uint256 end = periodEnd(missedPeriod); + require(end < block.timestamp, "Period has not ended yet"); + require(block.timestamp < end + config.timeout, "Validation timed out"); require(!received[id][missedPeriod], "Proof was submitted, not missing"); require(isProofRequired(id, missedPeriod), "Proof was not required"); require(!missing[id][missedPeriod], "Proof already marked as missing"); diff --git a/contracts/TestMarketplace.sol b/contracts/TestMarketplace.sol index 2e3a08b..99eabf9 100644 --- a/contracts/TestMarketplace.sol +++ b/contracts/TestMarketplace.sol @@ -6,26 +6,9 @@ import "./Marketplace.sol"; // exposes internal functions of Marketplace for testing contract TestMarketplace is Marketplace { constructor( - IERC20 _token, - uint256 _collateral, - uint256 _minCollateralThreshold, - uint256 _slashMisses, - uint256 _slashPercentage, - uint256 _proofPeriod, - uint256 _proofTimeout, - uint8 _proofDowntime - ) - Marketplace( - _token, - _collateral, - _minCollateralThreshold, - _slashMisses, - _slashPercentage, - _proofPeriod, - _proofTimeout, - _proofDowntime - ) - // solhint-disable-next-line no-empty-blocks + IERC20 token, + MarketplaceConfig memory config + ) Marketplace(token, config) // solhint-disable-next-line no-empty-blocks { } diff --git a/contracts/TestProofs.sol b/contracts/TestProofs.sol index e4f1f4c..6a59bf3 100644 --- a/contracts/TestProofs.sol +++ b/contracts/TestProofs.sol @@ -7,16 +7,8 @@ import "./Proofs.sol"; contract TestProofs is Proofs { mapping(SlotId => SlotState) private states; - constructor( - uint256 __period, - uint256 __timeout, - uint8 __downtime - ) - Proofs(__period, __timeout, __downtime) // solhint-disable-next-line no-empty-blocks - { - - } + constructor(ProofConfig memory config) Proofs(config) {} function slotState(SlotId slotId) internal view override returns (SlotState) { return states[slotId]; diff --git a/test/Marketplace.test.js b/test/Marketplace.test.js index 52be8db..e3c913e 100644 --- a/test/Marketplace.test.js +++ b/test/Marketplace.test.js @@ -3,7 +3,7 @@ const { hexlify, randomBytes } = ethers.utils const { AddressZero } = ethers.constants const { BigNumber } = ethers const { expect } = require("chai") -const { exampleRequest } = require("./examples") +const { exampleConfiguration, exampleRequest } = require("./examples") const { periodic, hours } = require("./time") const { requestId, slotId, askToArray } = require("./ids") const { RequestState } = require("./requests") @@ -26,14 +26,8 @@ const { } = require("./evm") describe("Marketplace", function () { - const collateral = 100 - const minCollateralThreshold = 40 - const slashMisses = 3 - const slashPercentage = 10 - const proofPeriod = 10 - const proofTimeout = 5 - const proofDowntime = 64 const proof = hexlify(randomBytes(42)) + const config = exampleConfiguration() let marketplace let token @@ -54,16 +48,7 @@ describe("Marketplace", function () { } const Marketplace = await ethers.getContractFactory("TestMarketplace") - marketplace = await Marketplace.deploy( - token.address, - collateral, - minCollateralThreshold, - slashMisses, - slashPercentage, - proofPeriod, - proofTimeout, - proofDowntime - ) + marketplace = await Marketplace.deploy(token.address, config) request = await exampleRequest() request.client = client.address @@ -136,8 +121,8 @@ describe("Marketplace", function () { await token.approve(marketplace.address, price(request)) await marketplace.requestStorage(request) switchAccount(host) - await token.approve(marketplace.address, collateral) - await marketplace.deposit(collateral) + await token.approve(marketplace.address, config.collateral.initialAmount) + await marketplace.deposit(config.collateral.initialAmount) }) it("emits event when slot is filled", async function () { @@ -160,7 +145,7 @@ describe("Marketplace", function () { }) it("is rejected when collateral is insufficient", async function () { - let insufficient = collateral - 1 + let insufficient = config.collateral.initialAmount - 1 await marketplace.withdraw() await token.approve(marketplace.address, insufficient) await marketplace.deposit(insufficient) @@ -236,8 +221,8 @@ describe("Marketplace", function () { await marketplace.requestStorage(request) requestTime = await currentTime() switchAccount(host) - await token.approve(marketplace.address, collateral) - await marketplace.deposit(collateral) + await token.approve(marketplace.address, config.collateral.initialAmount) + await marketplace.deposit(config.collateral.initialAmount) }) it("sets the request end time to now + duration", async function () { @@ -289,8 +274,8 @@ describe("Marketplace", function () { await token.approve(marketplace.address, price(request)) await marketplace.requestStorage(request) switchAccount(host) - await token.approve(marketplace.address, collateral) - await marketplace.deposit(collateral) + await token.approve(marketplace.address, config.collateral.initialAmount) + await marketplace.deposit(config.collateral.initialAmount) }) it("fails to free slot when slot not filled", async function () { @@ -328,8 +313,8 @@ describe("Marketplace", function () { await token.approve(marketplace.address, price(request)) await marketplace.requestStorage(request) switchAccount(host) - await token.approve(marketplace.address, collateral) - await marketplace.deposit(collateral) + await token.approve(marketplace.address, config.collateral.initialAmount) + await marketplace.deposit(config.collateral.initialAmount) }) it("pays the host when contract has finished", async function () { @@ -382,8 +367,8 @@ describe("Marketplace", function () { await token.approve(marketplace.address, price(request)) await marketplace.requestStorage(request) switchAccount(host) - await token.approve(marketplace.address, collateral) - await marketplace.deposit(collateral) + await token.approve(marketplace.address, config.collateral.initialAmount) + await marketplace.deposit(config.collateral.initialAmount) }) it("emits event when all slots are filled", async function () { @@ -421,8 +406,8 @@ describe("Marketplace", function () { await token.approve(marketplace.address, price(request)) await marketplace.requestStorage(request) switchAccount(host) - await token.approve(marketplace.address, collateral) - await marketplace.deposit(collateral) + await token.approve(marketplace.address, config.collateral.initialAmount) + await marketplace.deposit(config.collateral.initialAmount) }) it("rejects withdraw when request not yet timed out", async function () { @@ -476,8 +461,8 @@ describe("Marketplace", function () { await token.approve(marketplace.address, price(request)) await marketplace.requestStorage(request) switchAccount(host) - await token.approve(marketplace.address, collateral) - await marketplace.deposit(collateral) + await token.approve(marketplace.address, config.collateral.initialAmount) + await marketplace.deposit(config.collateral.initialAmount) }) it("locks collateral of host when it fills a slot", async function () { @@ -504,8 +489,8 @@ describe("Marketplace", function () { await token.approve(marketplace.address, price(request)) await marketplace.requestStorage(request) switchAccount(host) - await token.approve(marketplace.address, collateral) - await marketplace.deposit(collateral) + await token.approve(marketplace.address, config.collateral.initialAmount) + await marketplace.deposit(config.collateral.initialAmount) }) it("changes state to Cancelled when client withdraws funds", async function () { @@ -572,15 +557,15 @@ describe("Marketplace", function () { let period, periodOf, periodEnd beforeEach(async function () { - period = (await marketplace.proofPeriod()).toNumber() + period = config.proofs.period ;({ periodOf, periodEnd } = periodic(period)) switchAccount(client) await token.approve(marketplace.address, price(request)) await marketplace.requestStorage(request) switchAccount(host) - await token.approve(marketplace.address, collateral) - await marketplace.deposit(collateral) + await token.approve(marketplace.address, config.collateral.initialAmount) + await marketplace.deposit(config.collateral.initialAmount) }) async function waitUntilProofWillBeRequired(id) { @@ -652,15 +637,15 @@ describe("Marketplace", function () { let period, periodOf, periodEnd beforeEach(async function () { - period = (await marketplace.proofPeriod()).toNumber() + period = config.proofs.period ;({ periodOf, periodEnd } = periodic(period)) switchAccount(client) await token.approve(marketplace.address, price(request)) await marketplace.requestStorage(request) switchAccount(host) - await token.approve(marketplace.address, collateral) - await marketplace.deposit(collateral) + await token.approve(marketplace.address, config.collateral.initialAmount) + await marketplace.deposit(config.collateral.initialAmount) }) async function waitUntilProofIsRequired(id) { @@ -687,14 +672,16 @@ describe("Marketplace", function () { describe("slashing when missing proofs", function () { it("reduces collateral when too many proofs are missing", async function () { const id = slotId(slot) + const { slashCriterion, slashPercentage, initialAmount } = + config.collateral await marketplace.fillSlot(slot.request, slot.index, proof) - for (let i = 0; i < slashMisses; i++) { + for (let i = 0; i < slashCriterion; i++) { await waitUntilProofIsRequired(id) let missedPeriod = periodOf(await currentTime()) await advanceTime(period) await marketplace.markProofAsMissing(id, missedPeriod) } - const expectedBalance = (collateral * (100 - slashPercentage)) / 100 + const expectedBalance = (initialAmount * (100 - slashPercentage)) / 100 expect(await marketplace.balanceOf(host.address)).to.equal( expectedBalance ) @@ -707,8 +694,8 @@ describe("Marketplace", function () { await waitUntilStarted(marketplace, request, proof) - // max slashes before dropping below collateral threshold - const maxSlashes = 10 + const maxSlashes = 10 // slashes before going below collateral minimum + const slashMisses = config.collateral.slashCriterion for (let i = 0; i < maxSlashes; i++) { for (let j = 0; j < slashMisses; j++) { await waitUntilProofIsRequired(id) @@ -731,8 +718,8 @@ describe("Marketplace", function () { describe("list of active requests", function () { beforeEach(async function () { switchAccount(host) - await token.approve(marketplace.address, collateral) - await marketplace.deposit(collateral) + await token.approve(marketplace.address, config.collateral.initialAmount) + await marketplace.deposit(config.collateral.initialAmount) switchAccount(client) await token.approve(marketplace.address, price(request)) }) @@ -781,8 +768,8 @@ describe("Marketplace", function () { await token.approve(marketplace.address, price(request)) await marketplace.requestStorage(request) switchAccount(host) - await token.approve(marketplace.address, collateral) - await marketplace.deposit(collateral) + await token.approve(marketplace.address, config.collateral.initialAmount) + await marketplace.deposit(config.collateral.initialAmount) }) it("adds slot to list when filling slot", async function () { diff --git a/test/Proofs.test.js b/test/Proofs.test.js index c417cc7..c1525f9 100644 --- a/test/Proofs.test.js +++ b/test/Proofs.test.js @@ -27,7 +27,7 @@ describe("Proofs", function () { await snapshot() await ensureMinimumBlockHeight(256) const Proofs = await ethers.getContractFactory("TestProofs") - proofs = await Proofs.deploy(period, timeout, downtime) + proofs = await Proofs.deploy({ period, timeout, downtime }) }) afterEach(async function () { diff --git a/test/examples.js b/test/examples.js index bf23af3..b57c4cc 100644 --- a/test/examples.js +++ b/test/examples.js @@ -3,6 +3,20 @@ const { hours } = require("./time") const { currentTime } = require("./evm") const { hexlify, randomBytes } = ethers.utils +const exampleConfiguration = () => ({ + collateral: { + initialAmount: 100, + minimumAmount: 40, + slashCriterion: 3, + slashPercentage: 10, + }, + proofs: { + period: 10, + timeout: 5, + downtime: 64, + }, +}) + const exampleRequest = async () => { const now = await currentTime() return { @@ -31,4 +45,4 @@ const exampleRequest = async () => { } } -module.exports = { exampleRequest } +module.exports = { exampleConfiguration, exampleRequest }