[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/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).

View File

@ -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");

View File

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

View File

@ -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];

View File

@ -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 () {

View File

@ -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 () {

View File

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