[marketplace] introduce MarketplaceConfiguration struct
Container for all configuration values, replaces separate constructor parameters and getters.
This commit is contained in:
parent
91ccc82d49
commit
ae70fd7c6f
|
@ -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
|
||||
}
|
|
@ -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).
|
||||
|
|
|
@ -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");
|
||||
|
|
|
@ -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
|
||||
{
|
||||
|
||||
}
|
||||
|
|
|
@ -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];
|
||||
|
|
|
@ -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 () {
|
||||
|
|
|
@ -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 () {
|
||||
|
|
|
@ -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 }
|
||||
|
|
Loading…
Reference in New Issue