[marketplace] introduce MarketplaceConfiguration struct

Container for all configuration values, replaces separate
constructor parameters and getters.
This commit is contained in:
Mark Spanbroek 2023-01-17 13:55:58 +01:00 committed by markspanbroek
parent 91ccc82d49
commit ae70fd7c6f
8 changed files with 101 additions and 124 deletions

View File

@ -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
}

View File

@ -4,6 +4,7 @@ pragma solidity ^0.8.8;
import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/utils/math/Math.sol"; import "@openzeppelin/contracts/utils/math/Math.sol";
import "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; import "@openzeppelin/contracts/utils/structs/EnumerableSet.sol";
import "./Configuration.sol";
import "./Requests.sol"; import "./Requests.sol";
import "./Collateral.sol"; import "./Collateral.sol";
import "./Proofs.sol"; import "./Proofs.sol";
@ -13,10 +14,7 @@ contract Marketplace is Collateral, Proofs, StateRetrieval {
using EnumerableSet for EnumerableSet.Bytes32Set; using EnumerableSet for EnumerableSet.Bytes32Set;
using Requests for Request; using Requests for Request;
uint256 public immutable collateral; MarketplaceConfig public config;
uint256 public immutable minCollateralThreshold;
uint256 public immutable slashMisses;
uint256 public immutable slashPercentage;
MarketplaceFunds private funds; MarketplaceFunds private funds;
mapping(RequestId => Request) private requests; mapping(RequestId => Request) private requests;
@ -24,23 +22,10 @@ contract Marketplace is Collateral, Proofs, StateRetrieval {
mapping(SlotId => Slot) private slots; mapping(SlotId => Slot) private slots;
constructor( constructor(
IERC20 _token, IERC20 token,
uint256 _collateral, MarketplaceConfig memory configuration
uint256 _minCollateralThreshold, ) Collateral(token) Proofs(configuration.proofs) marketplaceInvariant {
uint256 _slashMisses, config = configuration;
uint256 _slashPercentage,
uint256 _proofPeriod,
uint256 _proofTimeout,
uint8 _proofDowntime
)
Collateral(_token)
Proofs(_proofPeriod, _proofTimeout, _proofDowntime)
marketplaceInvariant
{
collateral = _collateral;
minCollateralThreshold = _minCollateralThreshold;
slashMisses = _slashMisses;
slashPercentage = _slashPercentage;
} }
function isWithdrawAllowed() internal view override returns (bool) { 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(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); _startRequiringProofs(slotId, request.ask.proofProbability);
submitProof(slotId, proof); submitProof(slotId, proof);
@ -120,10 +108,10 @@ contract Marketplace is Collateral, Proofs, StateRetrieval {
require(slotState(slotId) == SlotState.Filled, "Slot not accepting proofs"); require(slotState(slotId) == SlotState.Filled, "Slot not accepting proofs");
_markProofAsMissing(slotId, period); _markProofAsMissing(slotId, period);
address host = getHost(slotId); address host = getHost(slotId);
if (missingProofs(slotId) % slashMisses == 0) { if (missingProofs(slotId) % config.collateral.slashCriterion == 0) {
_slash(host, slashPercentage); _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 // When the collateral drops below the minimum threshold, the slot
// needs to be freed so that there is enough remaining collateral to be // needs to be freed so that there is enough remaining collateral to be
// distributed for repairs and rewards (with any leftover to be burnt). // distributed for repairs and rewards (with any leftover to be burnt).

View File

@ -1,21 +1,16 @@
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
pragma solidity ^0.8.8; pragma solidity ^0.8.8;
import "./Configuration.sol";
import "./Requests.sol"; import "./Requests.sol";
import "./Periods.sol"; import "./Periods.sol";
abstract contract Proofs is Periods { abstract contract Proofs is Periods {
uint256 public immutable proofTimeout; ProofConfig private config;
uint8 private immutable downtime;
constructor( constructor(ProofConfig memory _config) Periods(_config.period) {
uint256 __period,
uint256 __timeout,
uint8 __downtime
) Periods(__period) {
require(block.number > 256, "Insufficient block height"); require(block.number > 256, "Insufficient block height");
proofTimeout = __timeout; config = _config;
downtime = __downtime;
} }
mapping(SlotId => uint256) private slotStarts; mapping(SlotId => uint256) private slotStarts;
@ -26,10 +21,6 @@ abstract contract Proofs is Periods {
function slotState(SlotId id) internal view virtual returns (SlotState); 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) { function missingProofs(SlotId slotId) public view returns (uint256) {
return missed[slotId]; return missed[slotId];
} }
@ -79,7 +70,7 @@ abstract contract Proofs is Periods {
} }
pointer = getPointer(id, period); pointer = getPointer(id, period);
bytes32 challenge = getChallenge(pointer); bytes32 challenge = getChallenge(pointer);
uint256 probability = (probabilities[id] * (256 - downtime)) / 256; uint256 probability = (probabilities[id] * (256 - config.downtime)) / 256;
isRequired = uint256(challenge) % probability == 0; isRequired = uint256(challenge) % probability == 0;
} }
@ -90,7 +81,7 @@ abstract contract Proofs is Periods {
bool isRequired; bool isRequired;
uint8 pointer; uint8 pointer;
(isRequired, pointer) = _getProofRequirement(id, period); (isRequired, pointer) = _getProofRequirement(id, period);
return isRequired && pointer >= downtime; return isRequired && pointer >= config.downtime;
} }
function isProofRequired(SlotId id) public view returns (bool) { function isProofRequired(SlotId id) public view returns (bool) {
@ -101,7 +92,7 @@ abstract contract Proofs is Periods {
bool isRequired; bool isRequired;
uint8 pointer; uint8 pointer;
(isRequired, pointer) = _getProofRequirement(id, blockPeriod()); (isRequired, pointer) = _getProofRequirement(id, blockPeriod());
return isRequired && pointer < downtime; return isRequired && pointer < config.downtime;
} }
function submitProof(SlotId id, bytes calldata proof) public { function submitProof(SlotId id, bytes calldata proof) public {
@ -112,9 +103,9 @@ abstract contract Proofs is Periods {
} }
function _markProofAsMissing(SlotId id, Period missedPeriod) internal { function _markProofAsMissing(SlotId id, Period missedPeriod) internal {
uint256 periodEnd = periodEnd(missedPeriod); uint256 end = periodEnd(missedPeriod);
require(periodEnd < block.timestamp, "Period has not ended yet"); require(end < block.timestamp, "Period has not ended yet");
require(block.timestamp < periodEnd + proofTimeout, "Validation timed out"); require(block.timestamp < end + config.timeout, "Validation timed out");
require(!received[id][missedPeriod], "Proof was submitted, not missing"); require(!received[id][missedPeriod], "Proof was submitted, not missing");
require(isProofRequired(id, missedPeriod), "Proof was not required"); require(isProofRequired(id, missedPeriod), "Proof was not required");
require(!missing[id][missedPeriod], "Proof already marked as missing"); require(!missing[id][missedPeriod], "Proof already marked as missing");

View File

@ -6,26 +6,9 @@ import "./Marketplace.sol";
// exposes internal functions of Marketplace for testing // exposes internal functions of Marketplace for testing
contract TestMarketplace is Marketplace { contract TestMarketplace is Marketplace {
constructor( constructor(
IERC20 _token, IERC20 token,
uint256 _collateral, MarketplaceConfig memory config
uint256 _minCollateralThreshold, ) Marketplace(token, config) // solhint-disable-next-line no-empty-blocks
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
{ {
} }

View File

@ -7,16 +7,8 @@ import "./Proofs.sol";
contract TestProofs is Proofs { contract TestProofs is Proofs {
mapping(SlotId => SlotState) private states; mapping(SlotId => SlotState) private states;
constructor(
uint256 __period,
uint256 __timeout,
uint8 __downtime
)
Proofs(__period, __timeout, __downtime)
// solhint-disable-next-line no-empty-blocks // solhint-disable-next-line no-empty-blocks
{ constructor(ProofConfig memory config) Proofs(config) {}
}
function slotState(SlotId slotId) internal view override returns (SlotState) { function slotState(SlotId slotId) internal view override returns (SlotState) {
return states[slotId]; return states[slotId];

View File

@ -3,7 +3,7 @@ const { hexlify, randomBytes } = ethers.utils
const { AddressZero } = ethers.constants const { AddressZero } = ethers.constants
const { BigNumber } = ethers const { BigNumber } = ethers
const { expect } = require("chai") const { expect } = require("chai")
const { exampleRequest } = require("./examples") const { exampleConfiguration, exampleRequest } = require("./examples")
const { periodic, hours } = require("./time") const { periodic, hours } = require("./time")
const { requestId, slotId, askToArray } = require("./ids") const { requestId, slotId, askToArray } = require("./ids")
const { RequestState } = require("./requests") const { RequestState } = require("./requests")
@ -26,14 +26,8 @@ const {
} = require("./evm") } = require("./evm")
describe("Marketplace", function () { 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 proof = hexlify(randomBytes(42))
const config = exampleConfiguration()
let marketplace let marketplace
let token let token
@ -54,16 +48,7 @@ describe("Marketplace", function () {
} }
const Marketplace = await ethers.getContractFactory("TestMarketplace") const Marketplace = await ethers.getContractFactory("TestMarketplace")
marketplace = await Marketplace.deploy( marketplace = await Marketplace.deploy(token.address, config)
token.address,
collateral,
minCollateralThreshold,
slashMisses,
slashPercentage,
proofPeriod,
proofTimeout,
proofDowntime
)
request = await exampleRequest() request = await exampleRequest()
request.client = client.address request.client = client.address
@ -136,8 +121,8 @@ describe("Marketplace", function () {
await token.approve(marketplace.address, price(request)) await token.approve(marketplace.address, price(request))
await marketplace.requestStorage(request) await marketplace.requestStorage(request)
switchAccount(host) switchAccount(host)
await token.approve(marketplace.address, collateral) await token.approve(marketplace.address, config.collateral.initialAmount)
await marketplace.deposit(collateral) await marketplace.deposit(config.collateral.initialAmount)
}) })
it("emits event when slot is filled", async function () { 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 () { it("is rejected when collateral is insufficient", async function () {
let insufficient = collateral - 1 let insufficient = config.collateral.initialAmount - 1
await marketplace.withdraw() await marketplace.withdraw()
await token.approve(marketplace.address, insufficient) await token.approve(marketplace.address, insufficient)
await marketplace.deposit(insufficient) await marketplace.deposit(insufficient)
@ -236,8 +221,8 @@ describe("Marketplace", function () {
await marketplace.requestStorage(request) await marketplace.requestStorage(request)
requestTime = await currentTime() requestTime = await currentTime()
switchAccount(host) switchAccount(host)
await token.approve(marketplace.address, collateral) await token.approve(marketplace.address, config.collateral.initialAmount)
await marketplace.deposit(collateral) await marketplace.deposit(config.collateral.initialAmount)
}) })
it("sets the request end time to now + duration", async function () { 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 token.approve(marketplace.address, price(request))
await marketplace.requestStorage(request) await marketplace.requestStorage(request)
switchAccount(host) switchAccount(host)
await token.approve(marketplace.address, collateral) await token.approve(marketplace.address, config.collateral.initialAmount)
await marketplace.deposit(collateral) await marketplace.deposit(config.collateral.initialAmount)
}) })
it("fails to free slot when slot not filled", async function () { 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 token.approve(marketplace.address, price(request))
await marketplace.requestStorage(request) await marketplace.requestStorage(request)
switchAccount(host) switchAccount(host)
await token.approve(marketplace.address, collateral) await token.approve(marketplace.address, config.collateral.initialAmount)
await marketplace.deposit(collateral) await marketplace.deposit(config.collateral.initialAmount)
}) })
it("pays the host when contract has finished", async function () { 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 token.approve(marketplace.address, price(request))
await marketplace.requestStorage(request) await marketplace.requestStorage(request)
switchAccount(host) switchAccount(host)
await token.approve(marketplace.address, collateral) await token.approve(marketplace.address, config.collateral.initialAmount)
await marketplace.deposit(collateral) await marketplace.deposit(config.collateral.initialAmount)
}) })
it("emits event when all slots are filled", async function () { 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 token.approve(marketplace.address, price(request))
await marketplace.requestStorage(request) await marketplace.requestStorage(request)
switchAccount(host) switchAccount(host)
await token.approve(marketplace.address, collateral) await token.approve(marketplace.address, config.collateral.initialAmount)
await marketplace.deposit(collateral) await marketplace.deposit(config.collateral.initialAmount)
}) })
it("rejects withdraw when request not yet timed out", async function () { 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 token.approve(marketplace.address, price(request))
await marketplace.requestStorage(request) await marketplace.requestStorage(request)
switchAccount(host) switchAccount(host)
await token.approve(marketplace.address, collateral) await token.approve(marketplace.address, config.collateral.initialAmount)
await marketplace.deposit(collateral) await marketplace.deposit(config.collateral.initialAmount)
}) })
it("locks collateral of host when it fills a slot", async function () { 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 token.approve(marketplace.address, price(request))
await marketplace.requestStorage(request) await marketplace.requestStorage(request)
switchAccount(host) switchAccount(host)
await token.approve(marketplace.address, collateral) await token.approve(marketplace.address, config.collateral.initialAmount)
await marketplace.deposit(collateral) await marketplace.deposit(config.collateral.initialAmount)
}) })
it("changes state to Cancelled when client withdraws funds", async function () { it("changes state to Cancelled when client withdraws funds", async function () {
@ -572,15 +557,15 @@ describe("Marketplace", function () {
let period, periodOf, periodEnd let period, periodOf, periodEnd
beforeEach(async function () { beforeEach(async function () {
period = (await marketplace.proofPeriod()).toNumber() period = config.proofs.period
;({ periodOf, periodEnd } = periodic(period)) ;({ periodOf, periodEnd } = periodic(period))
switchAccount(client) switchAccount(client)
await token.approve(marketplace.address, price(request)) await token.approve(marketplace.address, price(request))
await marketplace.requestStorage(request) await marketplace.requestStorage(request)
switchAccount(host) switchAccount(host)
await token.approve(marketplace.address, collateral) await token.approve(marketplace.address, config.collateral.initialAmount)
await marketplace.deposit(collateral) await marketplace.deposit(config.collateral.initialAmount)
}) })
async function waitUntilProofWillBeRequired(id) { async function waitUntilProofWillBeRequired(id) {
@ -652,15 +637,15 @@ describe("Marketplace", function () {
let period, periodOf, periodEnd let period, periodOf, periodEnd
beforeEach(async function () { beforeEach(async function () {
period = (await marketplace.proofPeriod()).toNumber() period = config.proofs.period
;({ periodOf, periodEnd } = periodic(period)) ;({ periodOf, periodEnd } = periodic(period))
switchAccount(client) switchAccount(client)
await token.approve(marketplace.address, price(request)) await token.approve(marketplace.address, price(request))
await marketplace.requestStorage(request) await marketplace.requestStorage(request)
switchAccount(host) switchAccount(host)
await token.approve(marketplace.address, collateral) await token.approve(marketplace.address, config.collateral.initialAmount)
await marketplace.deposit(collateral) await marketplace.deposit(config.collateral.initialAmount)
}) })
async function waitUntilProofIsRequired(id) { async function waitUntilProofIsRequired(id) {
@ -687,14 +672,16 @@ describe("Marketplace", function () {
describe("slashing when missing proofs", function () { describe("slashing when missing proofs", function () {
it("reduces collateral when too many proofs are missing", async function () { it("reduces collateral when too many proofs are missing", async function () {
const id = slotId(slot) const id = slotId(slot)
const { slashCriterion, slashPercentage, initialAmount } =
config.collateral
await marketplace.fillSlot(slot.request, slot.index, proof) 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) await waitUntilProofIsRequired(id)
let missedPeriod = periodOf(await currentTime()) let missedPeriod = periodOf(await currentTime())
await advanceTime(period) await advanceTime(period)
await marketplace.markProofAsMissing(id, missedPeriod) 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( expect(await marketplace.balanceOf(host.address)).to.equal(
expectedBalance expectedBalance
) )
@ -707,8 +694,8 @@ describe("Marketplace", function () {
await waitUntilStarted(marketplace, request, proof) await waitUntilStarted(marketplace, request, proof)
// max slashes before dropping below collateral threshold const maxSlashes = 10 // slashes before going below collateral minimum
const maxSlashes = 10 const slashMisses = config.collateral.slashCriterion
for (let i = 0; i < maxSlashes; i++) { for (let i = 0; i < maxSlashes; i++) {
for (let j = 0; j < slashMisses; j++) { for (let j = 0; j < slashMisses; j++) {
await waitUntilProofIsRequired(id) await waitUntilProofIsRequired(id)
@ -731,8 +718,8 @@ describe("Marketplace", function () {
describe("list of active requests", function () { describe("list of active requests", function () {
beforeEach(async function () { beforeEach(async function () {
switchAccount(host) switchAccount(host)
await token.approve(marketplace.address, collateral) await token.approve(marketplace.address, config.collateral.initialAmount)
await marketplace.deposit(collateral) await marketplace.deposit(config.collateral.initialAmount)
switchAccount(client) switchAccount(client)
await token.approve(marketplace.address, price(request)) await token.approve(marketplace.address, price(request))
}) })
@ -781,8 +768,8 @@ describe("Marketplace", function () {
await token.approve(marketplace.address, price(request)) await token.approve(marketplace.address, price(request))
await marketplace.requestStorage(request) await marketplace.requestStorage(request)
switchAccount(host) switchAccount(host)
await token.approve(marketplace.address, collateral) await token.approve(marketplace.address, config.collateral.initialAmount)
await marketplace.deposit(collateral) await marketplace.deposit(config.collateral.initialAmount)
}) })
it("adds slot to list when filling slot", async function () { it("adds slot to list when filling slot", async function () {

View File

@ -27,7 +27,7 @@ describe("Proofs", function () {
await snapshot() await snapshot()
await ensureMinimumBlockHeight(256) await ensureMinimumBlockHeight(256)
const Proofs = await ethers.getContractFactory("TestProofs") const Proofs = await ethers.getContractFactory("TestProofs")
proofs = await Proofs.deploy(period, timeout, downtime) proofs = await Proofs.deploy({ period, timeout, downtime })
}) })
afterEach(async function () { afterEach(async function () {

View File

@ -3,6 +3,20 @@ const { hours } = require("./time")
const { currentTime } = require("./evm") const { currentTime } = require("./evm")
const { hexlify, randomBytes } = ethers.utils 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 exampleRequest = async () => {
const now = await currentTime() const now = await currentTime()
return { return {
@ -31,4 +45,4 @@ const exampleRequest = async () => {
} }
} }
module.exports = { exampleRequest } module.exports = { exampleConfiguration, exampleRequest }