mirror of
https://github.com/logos-storage/logos-storage-contracts-eth.git
synced 2026-01-02 13:23:10 +00:00
Merge b5ab3869b949f651a3ed9aeb914feed7bd0dfd47 into 2dddc260152b6e9c24ae372397f9b9b2d27ce8e4
This commit is contained in:
commit
5a74b83c4c
5
.github/workflows/ci.yml
vendored
5
.github/workflows/ci.yml
vendored
@ -32,11 +32,6 @@ jobs:
|
||||
node-version: 18.15
|
||||
- run: npm install
|
||||
- run: npm test
|
||||
- uses: actions/cache@v4
|
||||
with:
|
||||
path: fuzzing/corpus
|
||||
key: fuzzing
|
||||
- run: npm run fuzz
|
||||
|
||||
verify:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
@ -14,10 +14,6 @@ To run the tests, execute the following commands:
|
||||
npm install
|
||||
npm test
|
||||
|
||||
You can also run fuzzing tests (using [Echidna][echidna]) on the contracts:
|
||||
|
||||
npm run fuzz
|
||||
|
||||
To start a local Ethereum node with the contracts deployed, execute:
|
||||
|
||||
npm start
|
||||
|
||||
@ -2,12 +2,14 @@
|
||||
"files": [
|
||||
"certora/harness/MarketplaceHarness.sol",
|
||||
"contracts/Marketplace.sol",
|
||||
"contracts/Vault.sol",
|
||||
"contracts/Groth16Verifier.sol",
|
||||
"certora/helpers/ERC20A.sol",
|
||||
],
|
||||
"parametric_contracts": ["MarketplaceHarness"],
|
||||
"link" : [
|
||||
"MarketplaceHarness:_token=ERC20A",
|
||||
"Vault:_token=ERC20A",
|
||||
"MarketplaceHarness:_vault=Vault",
|
||||
"MarketplaceHarness:_verifier=Groth16Verifier"
|
||||
],
|
||||
"msg": "Verifying MarketplaceHarness",
|
||||
@ -18,5 +20,3 @@
|
||||
"optimistic_hashing": true,
|
||||
"hashing_length_bound": "512",
|
||||
}
|
||||
|
||||
|
||||
|
||||
@ -2,12 +2,14 @@
|
||||
"files": [
|
||||
"certora/harness/MarketplaceHarness.sol",
|
||||
"contracts/Marketplace.sol",
|
||||
"contracts/Vault.sol",
|
||||
"contracts/Groth16Verifier.sol",
|
||||
"certora/helpers/ERC20A.sol",
|
||||
],
|
||||
"parametric_contracts": ["MarketplaceHarness"],
|
||||
"link" : [
|
||||
"MarketplaceHarness:_token=ERC20A",
|
||||
"Vault:_token=ERC20A",
|
||||
"MarketplaceHarness:_vault=Vault",
|
||||
"MarketplaceHarness:_verifier=Groth16Verifier"
|
||||
],
|
||||
"msg": "Verifying StateChanges",
|
||||
|
||||
@ -2,19 +2,20 @@
|
||||
|
||||
pragma solidity ^0.8.28;
|
||||
|
||||
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
|
||||
import {Vault} from "../../contracts/Vault.sol";
|
||||
import {IGroth16Verifier} from "../../contracts/Groth16.sol";
|
||||
import {MarketplaceConfig} from "../../contracts/Configuration.sol";
|
||||
import {Marketplace} from "../../contracts/Marketplace.sol";
|
||||
import {RequestId, SlotId} from "../../contracts/Requests.sol";
|
||||
import {Requests} from "../../contracts/Requests.sol";
|
||||
import {Timestamp} from "../../contracts/Timestamps.sol";
|
||||
|
||||
contract MarketplaceHarness is Marketplace {
|
||||
constructor(MarketplaceConfig memory config, IERC20 token, IGroth16Verifier verifier)
|
||||
Marketplace(config, token, verifier)
|
||||
constructor(MarketplaceConfig memory config, Vault vault, IGroth16Verifier verifier)
|
||||
Marketplace(config, vault, verifier)
|
||||
{}
|
||||
|
||||
function publicPeriodEnd(Period period) public view returns (uint64) {
|
||||
function publicPeriodEnd(Period period) public view returns (Timestamp) {
|
||||
return _periodEnd(period);
|
||||
}
|
||||
|
||||
|
||||
@ -3,9 +3,7 @@ import "./shared.spec";
|
||||
using ERC20A as Token;
|
||||
|
||||
methods {
|
||||
function Token.balanceOf(address) external returns (uint256) envfree;
|
||||
function Token.totalSupply() external returns (uint256) envfree;
|
||||
function publicPeriodEnd(Periods.Period) external returns (uint64) envfree;
|
||||
function publicPeriodEnd(Periods.Period) external returns (Marketplace.Timestamp) envfree;
|
||||
function generateSlotId(Marketplace.RequestId, uint64) external returns (Marketplace.SlotId) envfree;
|
||||
}
|
||||
|
||||
@ -13,44 +11,12 @@ methods {
|
||||
| Ghosts and hooks |
|
||||
--------------------------------------------*/
|
||||
|
||||
ghost mathint sumOfBalances {
|
||||
init_state axiom sumOfBalances == 0;
|
||||
}
|
||||
|
||||
hook Sload uint256 balance Token._balances[KEY address addr] {
|
||||
require sumOfBalances >= to_mathint(balance);
|
||||
}
|
||||
|
||||
hook Sstore Token._balances[KEY address addr] uint256 newValue (uint256 oldValue) {
|
||||
sumOfBalances = sumOfBalances - oldValue + newValue;
|
||||
}
|
||||
|
||||
ghost mathint totalReceived;
|
||||
|
||||
hook Sload uint256 defaultValue currentContract._marketplaceTotals.received {
|
||||
require totalReceived >= to_mathint(defaultValue);
|
||||
}
|
||||
|
||||
hook Sstore currentContract._marketplaceTotals.received uint256 defaultValue (uint256 defaultValue_old) {
|
||||
totalReceived = totalReceived + defaultValue - defaultValue_old;
|
||||
}
|
||||
|
||||
ghost mathint totalSent;
|
||||
|
||||
hook Sload uint256 defaultValue currentContract._marketplaceTotals.sent {
|
||||
require totalSent >= to_mathint(defaultValue);
|
||||
}
|
||||
|
||||
hook Sstore currentContract._marketplaceTotals.sent uint256 defaultValue (uint256 defaultValue_old) {
|
||||
totalSent = totalSent + defaultValue - defaultValue_old;
|
||||
}
|
||||
|
||||
ghost uint64 lastBlockTimestampGhost;
|
||||
ghost Marketplace.Timestamp lastBlockTimestampGhost;
|
||||
|
||||
hook TIMESTAMP uint v {
|
||||
require v < max_uint64;
|
||||
require lastBlockTimestampGhost <= assert_uint64(v);
|
||||
lastBlockTimestampGhost = assert_uint64(v);
|
||||
require v < max_uint40;
|
||||
require lastBlockTimestampGhost <= assert_uint40(v);
|
||||
lastBlockTimestampGhost = assert_uint40(v);
|
||||
}
|
||||
|
||||
ghost mapping(MarketplaceHarness.SlotId => mapping(Periods.Period => bool)) _missingMirror {
|
||||
@ -121,13 +87,13 @@ hook Sstore _requestContexts[KEY MarketplaceHarness.RequestId RequestId].slotsFi
|
||||
slotsFilledGhost[RequestId] = defaultValue;
|
||||
}
|
||||
|
||||
ghost mapping(MarketplaceHarness.RequestId => uint64) endsAtGhost;
|
||||
ghost mapping(MarketplaceHarness.RequestId => Marketplace.Timestamp) endsAtGhost;
|
||||
|
||||
hook Sload uint64 defaultValue _requestContexts[KEY MarketplaceHarness.RequestId RequestId].endsAt {
|
||||
hook Sload Marketplace.Timestamp defaultValue _requestContexts[KEY MarketplaceHarness.RequestId RequestId].endsAt {
|
||||
require endsAtGhost[RequestId] == defaultValue;
|
||||
}
|
||||
|
||||
hook Sstore _requestContexts[KEY MarketplaceHarness.RequestId RequestId].endsAt uint64 defaultValue {
|
||||
hook Sstore _requestContexts[KEY MarketplaceHarness.RequestId RequestId].endsAt Marketplace.Timestamp defaultValue {
|
||||
endsAtGhost[RequestId] = defaultValue;
|
||||
}
|
||||
|
||||
@ -144,18 +110,11 @@ function canStartRequest(method f) returns bool {
|
||||
}
|
||||
|
||||
function canFinishRequest(method f) returns bool {
|
||||
return f.selector == sig:freeSlot(Marketplace.SlotId, address, address).selector ||
|
||||
f.selector == sig:freeSlot(Marketplace.SlotId).selector;
|
||||
return f.selector == sig:freeSlot(Marketplace.SlotId).selector;
|
||||
}
|
||||
|
||||
function canFailRequest(method f) returns bool {
|
||||
return f.selector == sig:markProofAsMissing(Marketplace.SlotId, Periods.Period).selector ||
|
||||
f.selector == sig:freeSlot(Marketplace.SlotId, address, address).selector ||
|
||||
f.selector == sig:freeSlot(Marketplace.SlotId).selector;
|
||||
}
|
||||
|
||||
function canMakeSlotPaid(method f) returns bool {
|
||||
return f.selector == sig:freeSlot(Marketplace.SlotId, address, address).selector ||
|
||||
f.selector == sig:freeSlot(Marketplace.SlotId).selector;
|
||||
}
|
||||
|
||||
@ -174,9 +133,6 @@ function slotAttributesAreConsistent(env e, Marketplace.SlotId slotId) {
|
||||
| Invariants |
|
||||
--------------------------------------------*/
|
||||
|
||||
invariant totalSupplyIsSumOfBalances()
|
||||
to_mathint(Token.totalSupply()) == sumOfBalances;
|
||||
|
||||
invariant requestStartedWhenSlotsFilled(env e, Marketplace.RequestId requestId, Marketplace.SlotId slotId)
|
||||
currentContract.requestState(e, requestId) == Marketplace.RequestState.Started => to_mathint(currentContract.getRequest(e, requestId).ask.slots) - slotsFilledGhost[requestId] <= to_mathint(currentContract.getRequest(e, requestId).ask.maxSlotLoss);
|
||||
|
||||
@ -198,25 +154,6 @@ invariant cancelledRequestAlwaysExpired(env e, Marketplace.RequestId requestId)
|
||||
currentContract.requestState(e, requestId) == Marketplace.RequestState.Cancelled =>
|
||||
currentContract.requestExpiry(e, requestId) < lastBlockTimestampGhost;
|
||||
|
||||
// STATUS - verified
|
||||
// failed request is always ended
|
||||
// https://prover.certora.com/output/6199/3c5e57311e474f26aa7d9e9481c5880a?anonymousKey=36e39932ee488bb35fe23e38d8d4091190e047af
|
||||
invariant failedRequestAlwaysEnded(env e, Marketplace.RequestId requestId)
|
||||
currentContract.requestState(e, requestId) == Marketplace.RequestState.Failed =>
|
||||
endsAtGhost[requestId] < lastBlockTimestampGhost;
|
||||
|
||||
// STATUS - verified
|
||||
// paid slot always has finished or cancelled request
|
||||
// https://prover.certora.com/output/6199/d0e165ed5d594f9fb477602af06cfeb1?anonymousKey=01ffaad46027786c38d78e5a41c03ce002032200
|
||||
invariant paidSlotAlwaysHasFinishedOrCancelledRequest(env e, Marketplace.SlotId slotId)
|
||||
currentContract.slotState(e, slotId) == Marketplace.SlotState.Paid =>
|
||||
currentContract.requestState(e, slotIdToRequestId[slotId]) == Marketplace.RequestState.Finished || currentContract.requestState(e, slotIdToRequestId[slotId]) == Marketplace.RequestState.Cancelled
|
||||
{ preserved {
|
||||
requireInvariant cancelledSlotAlwaysHasCancelledRequest(e, slotId);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/*--------------------------------------------
|
||||
| Properties |
|
||||
--------------------------------------------*/
|
||||
@ -228,35 +165,11 @@ rule sanity(env e, method f) {
|
||||
satisfy true;
|
||||
}
|
||||
|
||||
rule totalReceivedCannotDecrease(env e, method f) {
|
||||
mathint total_before = totalReceived;
|
||||
|
||||
calldataarg args;
|
||||
f(e, args);
|
||||
|
||||
mathint total_after = totalReceived;
|
||||
|
||||
assert total_after >= total_before;
|
||||
}
|
||||
|
||||
rule totalSentCannotDecrease(env e, method f) {
|
||||
mathint total_before = totalSent;
|
||||
|
||||
calldataarg args;
|
||||
f(e, args);
|
||||
|
||||
mathint total_after = totalSent;
|
||||
|
||||
assert total_after >= total_before;
|
||||
}
|
||||
|
||||
// https://prover.certora.com/output/6199/0b56a7cdb3f9466db08f2a4677eddaac?anonymousKey=351ce9d5561f6c2aff1b38942e307735428bb83f
|
||||
rule slotIsFailedOrFreeIfRequestHasFailed(env e, method f) {
|
||||
calldataarg args;
|
||||
Marketplace.SlotId slotId;
|
||||
|
||||
requireInvariant paidSlotAlwaysHasFinishedOrCancelledRequest(e, slotId);
|
||||
|
||||
Marketplace.RequestState requestStateBefore = currentContract.requestState(e, slotIdToRequestId[slotId]);
|
||||
f(e, args);
|
||||
Marketplace.RequestState requestAfter = currentContract.requestState(e, slotIdToRequestId[slotId]);
|
||||
@ -298,9 +211,6 @@ rule functionsCausingSlotStateChanges(env e, method f) {
|
||||
f(e, args);
|
||||
Marketplace.SlotState slotStateAfter = currentContract.slotState(e, slotId);
|
||||
|
||||
// SlotState.Finished -> SlotState.Paid
|
||||
assert slotStateBefore == Marketplace.SlotState.Finished && slotStateAfter == Marketplace.SlotState.Paid => canMakeSlotPaid(f);
|
||||
|
||||
// SlotState.Free -> SlotState.Filled
|
||||
assert (slotStateBefore == Marketplace.SlotState.Free || slotStateBefore == Marketplace.SlotState.Repair) && slotStateAfter == Marketplace.SlotState.Filled => canFillSlot(f);
|
||||
}
|
||||
@ -316,15 +226,11 @@ rule allowedSlotStateChanges(env e, method f) {
|
||||
f(e, args);
|
||||
Marketplace.SlotState slotStateAfter = currentContract.slotState(e, slotId);
|
||||
|
||||
// Cannot change from Paid
|
||||
assert slotStateBefore == Marketplace.SlotState.Paid => slotStateAfter == Marketplace.SlotState.Paid;
|
||||
|
||||
// SlotState.Cancelled -> SlotState.Cancelled || SlotState.Failed || Finished || Paid
|
||||
// SlotState.Cancelled -> SlotState.Cancelled || SlotState.Failed || Finished
|
||||
assert slotStateBefore == Marketplace.SlotState.Cancelled => (
|
||||
slotStateAfter == Marketplace.SlotState.Cancelled ||
|
||||
slotStateAfter == Marketplace.SlotState.Failed ||
|
||||
slotStateAfter == Marketplace.SlotState.Finished ||
|
||||
slotStateAfter == Marketplace.SlotState.Paid
|
||||
slotStateAfter == Marketplace.SlotState.Finished
|
||||
);
|
||||
|
||||
// SlotState.Filled only from Free or Repair
|
||||
@ -385,21 +291,3 @@ rule slotStateChangesOnlyOncePerFunctionCall(env e, method f) {
|
||||
|
||||
assert slotStateChangesCountAfter <= slotStateChangesCountBefore + 1;
|
||||
}
|
||||
|
||||
rule slotCanBeFreedAndPaidOnce {
|
||||
env e;
|
||||
Marketplace.SlotId slotId;
|
||||
address rewardRecipient;
|
||||
address collateralRecipient;
|
||||
|
||||
Marketplace.SlotState slotStateBefore = currentContract.slotState(e, slotId);
|
||||
require slotStateBefore != Marketplace.SlotState.Paid;
|
||||
freeSlot(e, slotId, rewardRecipient, collateralRecipient);
|
||||
|
||||
Marketplace.SlotState slotStateAfter = currentContract.slotState(e, slotId);
|
||||
require slotStateAfter == Marketplace.SlotState.Paid;
|
||||
|
||||
freeSlot@withrevert(e, slotId, rewardRecipient, collateralRecipient);
|
||||
|
||||
assert lastReverted;
|
||||
}
|
||||
|
||||
@ -20,7 +20,7 @@ rule allowedRequestStateChanges(env e, method f) {
|
||||
// we need to check for `freeSlot(slotId)` here to ensure it's being called with
|
||||
// the slotId we're interested in and not any other slotId (that may not pass the
|
||||
// required invariants)
|
||||
if (f.selector == sig:freeSlot(Marketplace.SlotId).selector || f.selector == sig:freeSlot(Marketplace.SlotId, address, address).selector) {
|
||||
if (f.selector == sig:freeSlot(Marketplace.SlotId).selector) {
|
||||
freeSlot(e, slotId);
|
||||
} else {
|
||||
f(e, args);
|
||||
|
||||
@ -2,12 +2,13 @@
|
||||
pragma solidity 0.8.28;
|
||||
|
||||
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
|
||||
import "./Timestamps.sol";
|
||||
|
||||
struct MarketplaceConfig {
|
||||
CollateralConfig collateral;
|
||||
ProofConfig proofs;
|
||||
SlotReservationsConfig reservations;
|
||||
uint64 requestDurationLimit;
|
||||
Duration requestDurationLimit;
|
||||
}
|
||||
|
||||
struct CollateralConfig {
|
||||
@ -19,8 +20,8 @@ struct CollateralConfig {
|
||||
}
|
||||
|
||||
struct ProofConfig {
|
||||
uint64 period; // proofs requirements are calculated per period (in seconds)
|
||||
uint64 timeout; // mark proofs as missing before the timeout (in seconds)
|
||||
Duration period; // proofs requirements are calculated per period (in seconds)
|
||||
Duration timeout; // mark proofs as missing before the timeout (in seconds)
|
||||
uint8 downtime; // ignore this much recent blocks for proof requirements
|
||||
// Ensures the pointer does not remain in downtime for many consecutive
|
||||
// periods. For each period increase, move the pointer `pointerProduct`
|
||||
|
||||
@ -1,39 +0,0 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
pragma solidity ^0.8.28;
|
||||
|
||||
import "./TestToken.sol";
|
||||
import "./Marketplace.sol";
|
||||
import "./TestVerifier.sol";
|
||||
|
||||
contract FuzzMarketplace is Marketplace {
|
||||
constructor()
|
||||
Marketplace(
|
||||
MarketplaceConfig(
|
||||
CollateralConfig(10, 5, 10, 20),
|
||||
ProofConfig(10, 5, 64, 67, ""),
|
||||
SlotReservationsConfig(20),
|
||||
60 * 60 * 24 * 30 // 30 days
|
||||
),
|
||||
new TestToken(),
|
||||
new TestVerifier()
|
||||
)
|
||||
// solhint-disable-next-line no-empty-blocks
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
// Properties to be tested through fuzzing
|
||||
|
||||
MarketplaceTotals private _lastSeenTotals;
|
||||
|
||||
function neverDecreaseTotals() public {
|
||||
assert(_marketplaceTotals.received >= _lastSeenTotals.received);
|
||||
assert(_marketplaceTotals.sent >= _lastSeenTotals.sent);
|
||||
_lastSeenTotals = _marketplaceTotals;
|
||||
}
|
||||
|
||||
function neverLoseFunds() public view {
|
||||
uint256 total = _marketplaceTotals.received - _marketplaceTotals.sent;
|
||||
assert(token().balanceOf(address(this)) >= total);
|
||||
}
|
||||
}
|
||||
@ -2,8 +2,10 @@
|
||||
pragma solidity 0.8.28;
|
||||
|
||||
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
|
||||
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
|
||||
import "@openzeppelin/contracts/utils/math/Math.sol";
|
||||
import "@openzeppelin/contracts/utils/structs/EnumerableSet.sol";
|
||||
import "./Vault.sol";
|
||||
import "./Configuration.sol";
|
||||
import "./Requests.sol";
|
||||
import "./Proofs.sol";
|
||||
@ -11,11 +13,10 @@ import "./SlotReservations.sol";
|
||||
import "./StateRetrieval.sol";
|
||||
import "./Endian.sol";
|
||||
import "./Groth16.sol";
|
||||
import "./marketplace/VaultHelpers.sol";
|
||||
import "./marketplace/Collateral.sol";
|
||||
|
||||
contract Marketplace is SlotReservations, Proofs, StateRetrieval, Endian {
|
||||
error Marketplace_RepairRewardPercentageTooHigh();
|
||||
error Marketplace_SlashPercentageTooHigh();
|
||||
error Marketplace_MaximumSlashingTooHigh();
|
||||
error Marketplace_InvalidExpiry();
|
||||
error Marketplace_InvalidMaxSlotLoss();
|
||||
error Marketplace_InsufficientSlots();
|
||||
@ -29,8 +30,6 @@ contract Marketplace is SlotReservations, Proofs, StateRetrieval, Endian {
|
||||
error Marketplace_InvalidCid();
|
||||
error Marketplace_SlotNotFree();
|
||||
error Marketplace_InvalidSlotHost();
|
||||
error Marketplace_AlreadyPaid();
|
||||
error Marketplace_TransferFailed();
|
||||
error Marketplace_UnknownRequest();
|
||||
error Marketplace_InvalidState();
|
||||
error Marketplace_StartNotBeforeExpiry();
|
||||
@ -38,56 +37,40 @@ contract Marketplace is SlotReservations, Proofs, StateRetrieval, Endian {
|
||||
error Marketplace_ProofNotSubmittedByHost();
|
||||
error Marketplace_SlotIsFree();
|
||||
error Marketplace_ReservationRequired();
|
||||
error Marketplace_NothingToWithdraw();
|
||||
error Marketplace_DurationExceedsLimit();
|
||||
|
||||
using SafeERC20 for IERC20;
|
||||
using EnumerableSet for EnumerableSet.Bytes32Set;
|
||||
using EnumerableSet for EnumerableSet.AddressSet;
|
||||
using Requests for Request;
|
||||
using AskHelpers for Ask;
|
||||
using VaultHelpers for Vault;
|
||||
using VaultHelpers for RequestId;
|
||||
using VaultHelpers for Request;
|
||||
using Collateral for Request;
|
||||
using Collateral for CollateralConfig;
|
||||
using Timestamps for Timestamp;
|
||||
using Tokens for TokensPerSecond;
|
||||
|
||||
IERC20 private immutable _token;
|
||||
Vault private immutable _vault;
|
||||
MarketplaceConfig private _config;
|
||||
|
||||
mapping(RequestId => Request) private _requests;
|
||||
mapping(RequestId => RequestContext) internal _requestContexts;
|
||||
mapping(SlotId => Slot) internal _slots;
|
||||
|
||||
MarketplaceTotals internal _marketplaceTotals;
|
||||
|
||||
struct RequestContext {
|
||||
RequestState state;
|
||||
/// @notice Tracks how much funds should be returned to the client as not all funds might be used for hosting the request
|
||||
/// @dev The sum starts with the full reward amount for the request and is reduced every time a host fills a slot.
|
||||
/// The reduction is calculated from the duration of time between the slot being filled and the request's end.
|
||||
/// This is the amount that will be paid out to the host when the request successfully finishes.
|
||||
/// @dev fundsToReturnToClient == 0 is used to signal that after request is terminated all the remaining funds were withdrawn.
|
||||
/// This is possible, because technically it is not possible for this variable to reach 0 in "natural" way as
|
||||
/// that would require all the slots to be filled at the same block as the request was created.
|
||||
uint256 fundsToReturnToClient;
|
||||
uint64 slotsFilled;
|
||||
uint64 startedAt;
|
||||
uint64 endsAt;
|
||||
uint64 expiresAt;
|
||||
Timestamp endsAt;
|
||||
Timestamp expiresAt;
|
||||
}
|
||||
|
||||
struct Slot {
|
||||
SlotState state;
|
||||
RequestId requestId;
|
||||
/// @notice Timestamp that signals when slot was filled
|
||||
/// @dev Used for calculating payouts as hosts are paid
|
||||
/// based on time they actually host the content
|
||||
uint64 filledAt;
|
||||
uint64 slotIndex;
|
||||
/// @notice Tracks the current amount of host's collateral that is
|
||||
/// to be payed out at the end of Slot's lifespan.
|
||||
/// @dev When Slot is filled, the collateral is collected in amount
|
||||
/// of request.ask.collateralPerByte * request.ask.slotSize
|
||||
/// (== request.ask.collateralPerSlot() when using the AskHelpers library)
|
||||
/// @dev When Host is slashed for missing a proof the slashed amount is
|
||||
/// reflected in this variable
|
||||
uint256 currentCollateral;
|
||||
/// @notice address used for collateral interactions and identifying hosts
|
||||
uint128 currentCollateral;
|
||||
SlotState state;
|
||||
address host;
|
||||
}
|
||||
|
||||
@ -98,22 +81,11 @@ contract Marketplace is SlotReservations, Proofs, StateRetrieval, Endian {
|
||||
|
||||
constructor(
|
||||
MarketplaceConfig memory config,
|
||||
IERC20 token_,
|
||||
Vault vault_,
|
||||
IGroth16Verifier verifier
|
||||
) SlotReservations(config.reservations) Proofs(config.proofs, verifier) {
|
||||
_token = token_;
|
||||
|
||||
if (config.collateral.repairRewardPercentage > 100)
|
||||
revert Marketplace_RepairRewardPercentageTooHigh();
|
||||
if (config.collateral.slashPercentage > 100)
|
||||
revert Marketplace_SlashPercentageTooHigh();
|
||||
|
||||
if (
|
||||
config.collateral.maxNumberOfSlashes * config.collateral.slashPercentage >
|
||||
100
|
||||
) {
|
||||
revert Marketplace_MaximumSlashingTooHigh();
|
||||
}
|
||||
_vault = vault_;
|
||||
config.collateral.checkCorrectness();
|
||||
_config = config;
|
||||
}
|
||||
|
||||
@ -122,10 +94,14 @@ contract Marketplace is SlotReservations, Proofs, StateRetrieval, Endian {
|
||||
}
|
||||
|
||||
function token() public view returns (IERC20) {
|
||||
return _token;
|
||||
return _vault.getToken();
|
||||
}
|
||||
|
||||
function currentCollateral(SlotId slotId) public view returns (uint256) {
|
||||
function vault() public view returns (Vault) {
|
||||
return _vault;
|
||||
}
|
||||
|
||||
function currentCollateral(SlotId slotId) public view returns (uint128) {
|
||||
return _slots[slotId].currentCollateral;
|
||||
}
|
||||
|
||||
@ -136,12 +112,14 @@ contract Marketplace is SlotReservations, Proofs, StateRetrieval, Endian {
|
||||
if (_requests[id].client != address(0)) {
|
||||
revert Marketplace_RequestAlreadyExists();
|
||||
}
|
||||
if (request.expiry == 0 || request.expiry >= request.ask.duration)
|
||||
revert Marketplace_InvalidExpiry();
|
||||
if (
|
||||
request.expiry == Duration.wrap(0) ||
|
||||
request.expiry >= request.ask.duration
|
||||
) revert Marketplace_InvalidExpiry();
|
||||
if (request.ask.slots == 0) revert Marketplace_InsufficientSlots();
|
||||
if (request.ask.maxSlotLoss > request.ask.slots)
|
||||
revert Marketplace_InvalidMaxSlotLoss();
|
||||
if (request.ask.duration == 0) {
|
||||
if (request.ask.duration == Duration.wrap(0)) {
|
||||
revert Marketplace_InsufficientDuration();
|
||||
}
|
||||
if (request.ask.proofProbability == 0) {
|
||||
@ -150,7 +128,7 @@ contract Marketplace is SlotReservations, Proofs, StateRetrieval, Endian {
|
||||
if (request.ask.collateralPerByte == 0) {
|
||||
revert Marketplace_InsufficientCollateral();
|
||||
}
|
||||
if (request.ask.pricePerBytePerSecond == 0) {
|
||||
if (request.ask.pricePerBytePerSecond == TokensPerSecond.wrap(0)) {
|
||||
revert Marketplace_InsufficientReward();
|
||||
}
|
||||
if (bytes(request.content.cid).length == 0) {
|
||||
@ -160,20 +138,33 @@ contract Marketplace is SlotReservations, Proofs, StateRetrieval, Endian {
|
||||
revert Marketplace_DurationExceedsLimit();
|
||||
}
|
||||
|
||||
Timestamp currentTime = Timestamps.currentTime();
|
||||
Timestamp expiresAt = currentTime.add(request.expiry);
|
||||
Timestamp endsAt = currentTime.add(request.ask.duration);
|
||||
|
||||
_requests[id] = request;
|
||||
_requestContexts[id].endsAt =
|
||||
uint64(block.timestamp) +
|
||||
request.ask.duration;
|
||||
_requestContexts[id].expiresAt = uint64(block.timestamp) + request.expiry;
|
||||
_requestContexts[id] = RequestContext({
|
||||
state: RequestState.New,
|
||||
slotsFilled: 0,
|
||||
endsAt: endsAt,
|
||||
expiresAt: expiresAt
|
||||
});
|
||||
|
||||
_addToMyRequests(request.client, id);
|
||||
|
||||
uint256 amount = request.maxPrice();
|
||||
_requestContexts[id].fundsToReturnToClient = amount;
|
||||
_marketplaceTotals.received += amount;
|
||||
_transferFrom(msg.sender, amount);
|
||||
TokensPerSecond pricePerSecond = request.ask.pricePerSecond();
|
||||
uint128 price = pricePerSecond.accumulate(request.ask.duration);
|
||||
|
||||
emit StorageRequested(id, request.ask, _requestContexts[id].expiresAt);
|
||||
FundId fund = id.asFundId();
|
||||
AccountId account = _vault.clientAccount(request.client);
|
||||
_vault.lock(fund, expiresAt, endsAt);
|
||||
_transferToVault(request.client, fund, account, price);
|
||||
|
||||
// start flow from client to itself, to make sure that funds that are not
|
||||
// paid to hosts will slowly become designated to the client
|
||||
_vault.flow(fund, account, account, pricePerSecond);
|
||||
|
||||
emit StorageRequested(id, request.ask, expiresAt);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -210,30 +201,30 @@ contract Marketplace is SlotReservations, Proofs, StateRetrieval, Endian {
|
||||
}
|
||||
|
||||
slot.host = msg.sender;
|
||||
slot.filledAt = uint64(block.timestamp);
|
||||
|
||||
_startRequiringProofs(slotId);
|
||||
submitProof(slotId, proof);
|
||||
|
||||
context.slotsFilled += 1;
|
||||
context.fundsToReturnToClient -= _slotPayout(requestId, slot.filledAt);
|
||||
|
||||
// Collect collateral
|
||||
uint256 collateralAmount;
|
||||
uint256 collateralPerSlot = request.ask.collateralPerSlot();
|
||||
FundId fund = requestId.asFundId();
|
||||
AccountId clientAccount = _vault.clientAccount(request.client);
|
||||
AccountId hostAccount = _vault.hostAccount(slot.host, slotIndex);
|
||||
TokensPerSecond rate = request.ask.pricePerSlotPerSecond();
|
||||
|
||||
uint128 collateral = request.ask.collateralPerSlot();
|
||||
uint128 designated = _config.collateral.designatedCollateral(collateral);
|
||||
|
||||
if (slotState(slotId) == SlotState.Repair) {
|
||||
// Host is repairing a slot and is entitled for repair reward, so he gets "discounted collateral"
|
||||
// in this way he gets "physically" the reward at the end of the request when the full amount of collateral
|
||||
// is returned to him.
|
||||
collateralAmount =
|
||||
collateralPerSlot -
|
||||
((collateralPerSlot * _config.collateral.repairRewardPercentage) / 100);
|
||||
} else {
|
||||
collateralAmount = collateralPerSlot;
|
||||
uint128 repairReward = _config.collateral.repairReward(collateral);
|
||||
_vault.transfer(fund, clientAccount, hostAccount, repairReward);
|
||||
}
|
||||
_transferFrom(msg.sender, collateralAmount);
|
||||
_marketplaceTotals.received += collateralAmount;
|
||||
slot.currentCollateral = collateralPerSlot; // Even if he has collateral discounted, he is operating with full collateral
|
||||
|
||||
_transferToVault(slot.host, fund, hostAccount, collateral);
|
||||
_vault.designate(fund, hostAccount, designated);
|
||||
_vault.flow(fund, clientAccount, hostAccount, rate);
|
||||
|
||||
slot.currentCollateral = collateral;
|
||||
|
||||
_addToMySlots(slot.host, slotId);
|
||||
|
||||
@ -245,52 +236,40 @@ contract Marketplace is SlotReservations, Proofs, StateRetrieval, Endian {
|
||||
context.state == RequestState.New // Only New requests can "start" the requests
|
||||
) {
|
||||
context.state = RequestState.Started;
|
||||
context.startedAt = uint64(block.timestamp);
|
||||
_vault.extendLock(fund, context.endsAt);
|
||||
emit RequestFulfilled(requestId);
|
||||
}
|
||||
}
|
||||
|
||||
function _transferToVault(
|
||||
address from,
|
||||
FundId fund,
|
||||
AccountId account,
|
||||
uint128 amount
|
||||
) private {
|
||||
_vault.getToken().safeTransferFrom(from, address(this), amount);
|
||||
_vault.getToken().approve(address(_vault), amount);
|
||||
_vault.deposit(fund, account, amount);
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Frees a slot, paying out rewards and returning collateral for
|
||||
finished or cancelled requests to the host that has filled the slot.
|
||||
* @param slotId id of the slot to free
|
||||
* @dev The host that filled the slot must have initiated the transaction
|
||||
(msg.sender). This overload allows `rewardRecipient` and
|
||||
`collateralRecipient` to be optional.
|
||||
(msg.sender).
|
||||
*/
|
||||
function freeSlot(SlotId slotId) public slotIsNotFree(slotId) {
|
||||
return freeSlot(slotId, msg.sender, msg.sender);
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Frees a slot, paying out rewards and returning collateral for
|
||||
finished or cancelled requests.
|
||||
* @param slotId id of the slot to free
|
||||
* @param rewardRecipient address to send rewards to
|
||||
* @param collateralRecipient address to refund collateral to
|
||||
*/
|
||||
function freeSlot(
|
||||
SlotId slotId,
|
||||
address rewardRecipient,
|
||||
address collateralRecipient
|
||||
) public slotIsNotFree(slotId) {
|
||||
Slot storage slot = _slots[slotId];
|
||||
if (slot.host != msg.sender) revert Marketplace_InvalidSlotHost();
|
||||
|
||||
SlotState state = slotState(slotId);
|
||||
if (state == SlotState.Paid) revert Marketplace_AlreadyPaid();
|
||||
|
||||
if (state == SlotState.Finished) {
|
||||
_payoutSlot(slot.requestId, slotId, rewardRecipient, collateralRecipient);
|
||||
} else if (state == SlotState.Cancelled) {
|
||||
_payoutCancelledSlot(
|
||||
slot.requestId,
|
||||
slotId,
|
||||
rewardRecipient,
|
||||
collateralRecipient
|
||||
);
|
||||
} else if (state == SlotState.Failed) {
|
||||
_removeFromMySlots(msg.sender, slotId);
|
||||
if (
|
||||
state == SlotState.Finished ||
|
||||
state == SlotState.Cancelled ||
|
||||
state == SlotState.Failed
|
||||
) {
|
||||
_payoutSlot(slot.requestId, slotId);
|
||||
} else if (state == SlotState.Filled) {
|
||||
// free slot without returning collateral, effectively a 100% slash
|
||||
_forciblyFreeSlot(slotId);
|
||||
@ -351,16 +330,16 @@ contract Marketplace is SlotReservations, Proofs, StateRetrieval, Endian {
|
||||
Slot storage slot = _slots[slotId];
|
||||
Request storage request = _requests[slot.requestId];
|
||||
|
||||
uint256 slashedAmount = (request.ask.collateralPerSlot() *
|
||||
_config.collateral.slashPercentage) / 100;
|
||||
uint128 collateral = request.ask.collateralPerSlot();
|
||||
uint128 slashedAmount = _config.collateral.slashAmount(collateral);
|
||||
uint128 validatorReward = _config.collateral.validatorReward(slashedAmount);
|
||||
|
||||
uint256 validatorRewardAmount = (slashedAmount *
|
||||
_config.collateral.validatorRewardPercentage) / 100;
|
||||
_marketplaceTotals.sent += validatorRewardAmount;
|
||||
|
||||
if (!_token.transfer(msg.sender, validatorRewardAmount)) {
|
||||
revert Marketplace_TransferFailed();
|
||||
}
|
||||
FundId fund = slot.requestId.asFundId();
|
||||
AccountId hostAccount = _vault.hostAccount(slot.host, slot.slotIndex);
|
||||
AccountId validatorAccount = _vault.validatorAccount(msg.sender);
|
||||
_vault.transfer(fund, hostAccount, validatorAccount, validatorReward);
|
||||
_vault.designate(fund, validatorAccount, validatorReward);
|
||||
_vault.burnDesignated(fund, hostAccount, slashedAmount - validatorReward);
|
||||
|
||||
slot.currentCollateral -= slashedAmount;
|
||||
if (missingProofs(slotId) >= _config.collateral.maxNumberOfSlashes) {
|
||||
@ -370,176 +349,86 @@ contract Marketplace is SlotReservations, Proofs, StateRetrieval, Endian {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Abandons the slot without returning collateral, effectively slashing the
|
||||
entire collateral.
|
||||
* @param slotId SlotId of the slot to free.
|
||||
* @dev _slots[slotId] is deleted, resetting _slots[slotId].currentCollateral
|
||||
to 0.
|
||||
*/
|
||||
/// Abandons the slot, burns all associated tokens
|
||||
function _forciblyFreeSlot(SlotId slotId) internal {
|
||||
Slot storage slot = _slots[slotId];
|
||||
RequestId requestId = slot.requestId;
|
||||
RequestContext storage context = _requestContexts[requestId];
|
||||
|
||||
// We need to refund the amount of payout of the current node to the `fundsToReturnToClient` so
|
||||
// we keep correctly the track of the funds that needs to be returned at the end.
|
||||
context.fundsToReturnToClient += _slotPayout(requestId, slot.filledAt);
|
||||
Request storage request = _requests[requestId];
|
||||
|
||||
TokensPerSecond rate = request.ask.pricePerSlotPerSecond();
|
||||
uint128 collateral = request.ask.collateralPerSlot();
|
||||
uint128 repairReward = _config.collateral.repairReward(collateral);
|
||||
|
||||
FundId fund = requestId.asFundId();
|
||||
AccountId hostAccount = _vault.hostAccount(slot.host, slot.slotIndex);
|
||||
AccountId clientAccount = _vault.clientAccount(request.client);
|
||||
|
||||
// ensure that nothing is flowing into the account anymore by reversing the
|
||||
// incoming flow from the client
|
||||
_vault.flow(fund, hostAccount, clientAccount, rate);
|
||||
|
||||
// temporarily transfer repair reward for the slot to the client until a
|
||||
// host repairs the slot
|
||||
_vault.transfer(fund, hostAccount, clientAccount, repairReward);
|
||||
|
||||
// burn the rest of the funds in the account
|
||||
_vault.burnAccount(fund, hostAccount);
|
||||
|
||||
_removeFromMySlots(slot.host, slotId);
|
||||
_reservations[slotId].clear(); // We purge all the reservations for the slot
|
||||
slot.state = SlotState.Repair;
|
||||
slot.filledAt = 0;
|
||||
slot.currentCollateral = 0;
|
||||
slot.host = address(0);
|
||||
context.slotsFilled -= 1;
|
||||
emit SlotFreed(requestId, slot.slotIndex);
|
||||
_resetMissingProofs(slotId);
|
||||
|
||||
Request storage request = _requests[requestId];
|
||||
uint256 slotsLost = request.ask.slots - context.slotsFilled;
|
||||
if (
|
||||
slotsLost > request.ask.maxSlotLoss &&
|
||||
context.state == RequestState.Started
|
||||
) {
|
||||
context.state = RequestState.Failed;
|
||||
context.endsAt = uint64(block.timestamp) - 1;
|
||||
_vault.freezeFund(fund);
|
||||
|
||||
emit RequestFailed(requestId);
|
||||
}
|
||||
}
|
||||
|
||||
function _payoutSlot(
|
||||
RequestId requestId,
|
||||
SlotId slotId,
|
||||
address rewardRecipient,
|
||||
address collateralRecipient
|
||||
) private requestIsKnown(requestId) {
|
||||
RequestContext storage context = _requestContexts[requestId];
|
||||
Request storage request = _requests[requestId];
|
||||
context.state = RequestState.Finished;
|
||||
Slot storage slot = _slots[slotId];
|
||||
|
||||
_removeFromMyRequests(request.client, requestId);
|
||||
_removeFromMySlots(slot.host, slotId);
|
||||
|
||||
uint256 payoutAmount = _slotPayout(requestId, slot.filledAt);
|
||||
uint256 collateralAmount = slot.currentCollateral;
|
||||
_marketplaceTotals.sent += (payoutAmount + collateralAmount);
|
||||
slot.state = SlotState.Paid;
|
||||
if (!_token.transfer(rewardRecipient, payoutAmount)) {
|
||||
revert Marketplace_TransferFailed();
|
||||
}
|
||||
|
||||
if (!_token.transfer(collateralRecipient, collateralAmount)) {
|
||||
revert Marketplace_TransferFailed();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Pays out a host for duration of time that the slot was filled, and
|
||||
returns the collateral.
|
||||
* @dev The payouts are sent to the rewardRecipient, and collateral is returned
|
||||
to the host address.
|
||||
* @param requestId RequestId of the request that contains the slot to be paid
|
||||
out.
|
||||
* @param slotId SlotId of the slot to be paid out.
|
||||
*/
|
||||
function _payoutCancelledSlot(
|
||||
function _payoutSlot(
|
||||
RequestId requestId,
|
||||
SlotId slotId,
|
||||
address rewardRecipient,
|
||||
address collateralRecipient
|
||||
SlotId slotId
|
||||
) private requestIsKnown(requestId) {
|
||||
Slot storage slot = _slots[slotId];
|
||||
slot.currentCollateral = 0;
|
||||
_removeFromMySlots(slot.host, slotId);
|
||||
|
||||
uint256 payoutAmount = _slotPayout(
|
||||
requestId,
|
||||
slot.filledAt,
|
||||
requestExpiry(requestId)
|
||||
);
|
||||
uint256 collateralAmount = slot.currentCollateral;
|
||||
_marketplaceTotals.sent += (payoutAmount + collateralAmount);
|
||||
slot.state = SlotState.Paid;
|
||||
if (!_token.transfer(rewardRecipient, payoutAmount)) {
|
||||
revert Marketplace_TransferFailed();
|
||||
}
|
||||
|
||||
if (!_token.transfer(collateralRecipient, collateralAmount)) {
|
||||
revert Marketplace_TransferFailed();
|
||||
}
|
||||
FundId fund = requestId.asFundId();
|
||||
AccountId account = _vault.hostAccount(slot.host, slot.slotIndex);
|
||||
_vault.withdraw(fund, account);
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Withdraws remaining storage request funds back to the client that
|
||||
deposited them.
|
||||
* @dev Request must be cancelled, failed or finished, and the
|
||||
transaction must originate from the depositor address.
|
||||
* @param requestId the id of the request
|
||||
*/
|
||||
function withdrawFunds(RequestId requestId) public {
|
||||
withdrawFunds(requestId, msg.sender);
|
||||
/// Withdraws remaining storage request funds back to the client that
|
||||
function withdrawFunds(RequestId requestId) public requestIsKnown(requestId) {
|
||||
FundId fund = requestId.asFundId();
|
||||
AccountId account = _vault.clientAccount(msg.sender);
|
||||
_vault.withdraw(fund, account);
|
||||
|
||||
_removeFromMyRequests(msg.sender, requestId);
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Withdraws storage request funds to the provided address.
|
||||
* @dev Request must be expired, must be in RequestState.New, and the
|
||||
transaction must originate from the depositer address.
|
||||
* @param requestId the id of the request
|
||||
* @param withdrawRecipient address to return the remaining funds to
|
||||
*/
|
||||
function withdrawFunds(
|
||||
RequestId requestId,
|
||||
address withdrawRecipient
|
||||
) public requestIsKnown(requestId) {
|
||||
Request storage request = _requests[requestId];
|
||||
RequestContext storage context = _requestContexts[requestId];
|
||||
|
||||
if (request.client != msg.sender) revert Marketplace_InvalidClientAddress();
|
||||
|
||||
RequestState state = requestState(requestId);
|
||||
if (
|
||||
state != RequestState.Cancelled &&
|
||||
state != RequestState.Failed &&
|
||||
state != RequestState.Finished
|
||||
) {
|
||||
revert Marketplace_InvalidState();
|
||||
}
|
||||
|
||||
// fundsToReturnToClient == 0 is used for "double-spend" protection, once the funds are withdrawn
|
||||
// then this variable is set to 0.
|
||||
if (context.fundsToReturnToClient == 0)
|
||||
revert Marketplace_NothingToWithdraw();
|
||||
|
||||
if (state == RequestState.Cancelled) {
|
||||
context.state = RequestState.Cancelled;
|
||||
emit RequestCancelled(requestId);
|
||||
|
||||
// `fundsToReturnToClient` currently tracks funds to be returned for requests that successfully finish.
|
||||
// When requests are cancelled, funds earmarked for payment for the duration
|
||||
// between request expiry and request end (for every slot that was filled), should be returned to the client.
|
||||
// Update `fundsToReturnToClient` to reflect this.
|
||||
context.fundsToReturnToClient +=
|
||||
context.slotsFilled *
|
||||
_slotPayout(requestId, requestExpiry(requestId));
|
||||
} else if (state == RequestState.Failed) {
|
||||
// For Failed requests the client is refunded whole amount.
|
||||
context.fundsToReturnToClient = request.maxPrice();
|
||||
} else {
|
||||
context.state = RequestState.Finished;
|
||||
}
|
||||
|
||||
_removeFromMyRequests(request.client, requestId);
|
||||
|
||||
uint256 amount = context.fundsToReturnToClient;
|
||||
_marketplaceTotals.sent += amount;
|
||||
|
||||
if (!_token.transfer(withdrawRecipient, amount)) {
|
||||
revert Marketplace_TransferFailed();
|
||||
}
|
||||
|
||||
// We zero out the funds tracking in order to prevent double-spends
|
||||
context.fundsToReturnToClient = 0;
|
||||
function withdrawByValidator(RequestId requestId) public {
|
||||
FundId fund = requestId.asFundId();
|
||||
AccountId account = _vault.validatorAccount(msg.sender);
|
||||
_vault.withdraw(fund, account);
|
||||
}
|
||||
|
||||
function getActiveSlot(
|
||||
@ -580,55 +469,18 @@ contract Marketplace is SlotReservations, Proofs, StateRetrieval, Endian {
|
||||
_;
|
||||
}
|
||||
|
||||
function requestEnd(RequestId requestId) public view returns (uint64) {
|
||||
function requestEnd(RequestId requestId) public view returns (Timestamp) {
|
||||
RequestState state = requestState(requestId);
|
||||
if (state == RequestState.New || state == RequestState.Started) {
|
||||
return _requestContexts[requestId].endsAt;
|
||||
}
|
||||
if (state == RequestState.Cancelled) {
|
||||
return _requestContexts[requestId].expiresAt;
|
||||
}
|
||||
return
|
||||
uint64(Math.min(_requestContexts[requestId].endsAt, block.timestamp));
|
||||
return _requestContexts[requestId].endsAt;
|
||||
}
|
||||
|
||||
function requestExpiry(RequestId requestId) public view returns (uint64) {
|
||||
function requestExpiry(RequestId requestId) public view returns (Timestamp) {
|
||||
return _requestContexts[requestId].expiresAt;
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Calculates the amount that should be paid out to a host that successfully finished the request
|
||||
* @param requestId RequestId of the request used to calculate the payout
|
||||
* amount.
|
||||
* @param startingTimestamp timestamp indicating when a host filled a slot and
|
||||
* started providing proofs.
|
||||
*/
|
||||
function _slotPayout(
|
||||
RequestId requestId,
|
||||
uint64 startingTimestamp
|
||||
) private view returns (uint256) {
|
||||
return
|
||||
_slotPayout(
|
||||
requestId,
|
||||
startingTimestamp,
|
||||
_requestContexts[requestId].endsAt
|
||||
);
|
||||
}
|
||||
|
||||
/// @notice Calculates the amount that should be paid out to a host based on the specified time frame.
|
||||
function _slotPayout(
|
||||
RequestId requestId,
|
||||
uint64 startingTimestamp,
|
||||
uint64 endingTimestamp
|
||||
) private view returns (uint256) {
|
||||
Request storage request = _requests[requestId];
|
||||
if (startingTimestamp >= endingTimestamp)
|
||||
revert Marketplace_StartNotBeforeExpiry();
|
||||
return
|
||||
(endingTimestamp - startingTimestamp) *
|
||||
request.ask.pricePerSlotPerSecond();
|
||||
}
|
||||
|
||||
function getHost(SlotId slotId) public view returns (address) {
|
||||
return _slots[slotId].host;
|
||||
}
|
||||
@ -637,15 +489,15 @@ contract Marketplace is SlotReservations, Proofs, StateRetrieval, Endian {
|
||||
RequestId requestId
|
||||
) public view requestIsKnown(requestId) returns (RequestState) {
|
||||
RequestContext storage context = _requestContexts[requestId];
|
||||
Timestamp currentTime = Timestamps.currentTime();
|
||||
if (
|
||||
context.state == RequestState.New &&
|
||||
uint64(block.timestamp) > requestExpiry(requestId)
|
||||
requestExpiry(requestId) < currentTime
|
||||
) {
|
||||
return RequestState.Cancelled;
|
||||
} else if (
|
||||
(context.state == RequestState.Started ||
|
||||
context.state == RequestState.New) &&
|
||||
uint64(block.timestamp) > context.endsAt
|
||||
context.state == RequestState.New) && context.endsAt < currentTime
|
||||
) {
|
||||
return RequestState.Finished;
|
||||
} else {
|
||||
@ -661,9 +513,6 @@ contract Marketplace is SlotReservations, Proofs, StateRetrieval, Endian {
|
||||
return SlotState.Free;
|
||||
}
|
||||
RequestState reqState = requestState(slot.requestId);
|
||||
if (slot.state == SlotState.Paid) {
|
||||
return SlotState.Paid;
|
||||
}
|
||||
if (reqState == RequestState.Cancelled) {
|
||||
return SlotState.Cancelled;
|
||||
}
|
||||
@ -685,21 +534,9 @@ contract Marketplace is SlotReservations, Proofs, StateRetrieval, Endian {
|
||||
(request.ask.proofProbability * (256 - _config.proofs.downtime)) / 256;
|
||||
}
|
||||
|
||||
function _transferFrom(address sender, uint256 amount) internal {
|
||||
address receiver = address(this);
|
||||
if (!_token.transferFrom(sender, receiver, amount))
|
||||
revert Marketplace_TransferFailed();
|
||||
}
|
||||
|
||||
event StorageRequested(RequestId requestId, Ask ask, uint64 expiry);
|
||||
event StorageRequested(RequestId requestId, Ask ask, Timestamp expiry);
|
||||
event RequestFulfilled(RequestId indexed requestId);
|
||||
event RequestFailed(RequestId indexed requestId);
|
||||
event SlotFilled(RequestId indexed requestId, uint64 slotIndex);
|
||||
event SlotFreed(RequestId indexed requestId, uint64 slotIndex);
|
||||
event RequestCancelled(RequestId indexed requestId);
|
||||
|
||||
struct MarketplaceTotals {
|
||||
uint256 received;
|
||||
uint256 sent;
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,37 +1,45 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
pragma solidity 0.8.28;
|
||||
|
||||
import "./Timestamps.sol";
|
||||
|
||||
contract Periods {
|
||||
error Periods_InvalidSecondsPerPeriod();
|
||||
|
||||
type Period is uint64;
|
||||
type Period is uint40;
|
||||
|
||||
uint64 internal immutable _secondsPerPeriod;
|
||||
Duration internal immutable _secondsPerPeriod;
|
||||
|
||||
constructor(uint64 secondsPerPeriod) {
|
||||
if (secondsPerPeriod == 0) {
|
||||
constructor(Duration secondsPerPeriod) {
|
||||
if (secondsPerPeriod == Duration.wrap(0)) {
|
||||
revert Periods_InvalidSecondsPerPeriod();
|
||||
}
|
||||
_secondsPerPeriod = secondsPerPeriod;
|
||||
}
|
||||
|
||||
function _periodOf(uint64 timestamp) internal view returns (Period) {
|
||||
return Period.wrap(timestamp / _secondsPerPeriod);
|
||||
function _periodOf(Timestamp timestamp) internal view returns (Period) {
|
||||
return
|
||||
Period.wrap(
|
||||
Timestamp.unwrap(timestamp) / Duration.unwrap(_secondsPerPeriod)
|
||||
);
|
||||
}
|
||||
|
||||
function _blockPeriod() internal view returns (Period) {
|
||||
return _periodOf(uint64(block.timestamp));
|
||||
return _periodOf(Timestamps.currentTime());
|
||||
}
|
||||
|
||||
function _nextPeriod(Period period) internal pure returns (Period) {
|
||||
return Period.wrap(Period.unwrap(period) + 1);
|
||||
}
|
||||
|
||||
function _periodStart(Period period) internal view returns (uint64) {
|
||||
return Period.unwrap(period) * _secondsPerPeriod;
|
||||
function _periodStart(Period period) internal view returns (Timestamp) {
|
||||
return
|
||||
Timestamp.wrap(
|
||||
Period.unwrap(period) * Duration.unwrap(_secondsPerPeriod)
|
||||
);
|
||||
}
|
||||
|
||||
function _periodEnd(Period period) internal view returns (uint64) {
|
||||
function _periodEnd(Period period) internal view returns (Timestamp) {
|
||||
return _periodStart(_nextPeriod(period));
|
||||
}
|
||||
|
||||
|
||||
@ -3,6 +3,7 @@ pragma solidity 0.8.28;
|
||||
|
||||
import "./Configuration.sol";
|
||||
import "./Requests.sol";
|
||||
import "./Timestamps.sol";
|
||||
import "./Periods.sol";
|
||||
import "./Groth16.sol";
|
||||
|
||||
@ -11,6 +12,8 @@ import "./Groth16.sol";
|
||||
* @notice Abstract contract that handles proofs tracking, validation and reporting functionality
|
||||
*/
|
||||
abstract contract Proofs is Periods {
|
||||
using Timestamps for Timestamp;
|
||||
|
||||
error Proofs_InsufficientBlockHeight();
|
||||
error Proofs_InvalidProof();
|
||||
error Proofs_ProofAlreadySubmitted();
|
||||
@ -39,7 +42,7 @@ abstract contract Proofs is Periods {
|
||||
_verifier = verifier;
|
||||
}
|
||||
|
||||
mapping(SlotId => uint64) private _slotStarts;
|
||||
mapping(SlotId => Timestamp) private _slotStarts;
|
||||
mapping(SlotId => uint64) private _missed;
|
||||
mapping(SlotId => mapping(Period => bool)) private _received;
|
||||
mapping(SlotId => mapping(Period => bool)) private _missing;
|
||||
@ -73,7 +76,7 @@ abstract contract Proofs is Periods {
|
||||
* and saves the required probability.
|
||||
*/
|
||||
function _startRequiringProofs(SlotId id) internal {
|
||||
_slotStarts[id] = uint64(block.timestamp);
|
||||
_slotStarts[id] = Timestamps.currentTime();
|
||||
}
|
||||
|
||||
/**
|
||||
@ -234,10 +237,10 @@ abstract contract Proofs is Periods {
|
||||
SlotId id,
|
||||
Period missedPeriod
|
||||
) internal view {
|
||||
uint256 end = _periodEnd(missedPeriod);
|
||||
if (end >= block.timestamp) revert Proofs_PeriodNotEnded();
|
||||
if (block.timestamp >= end + _config.timeout)
|
||||
revert Proofs_ValidationTimedOut();
|
||||
Timestamp end = _periodEnd(missedPeriod);
|
||||
Timestamp current = Timestamps.currentTime();
|
||||
if (current <= end) revert Proofs_PeriodNotEnded();
|
||||
if (end.add(_config.timeout) <= current) revert Proofs_ValidationTimedOut();
|
||||
if (_received[id][missedPeriod]) revert Proofs_ProofNotMissing();
|
||||
if (!_isProofRequired(id, missedPeriod)) revert Proofs_ProofNotRequired();
|
||||
if (_missing[id][missedPeriod]) revert Proofs_ProofAlreadyMarkedMissing();
|
||||
|
||||
@ -1,6 +1,9 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
pragma solidity 0.8.28;
|
||||
|
||||
import "./Timestamps.sol";
|
||||
import "./Tokens.sol";
|
||||
|
||||
type RequestId is bytes32;
|
||||
type SlotId is bytes32;
|
||||
|
||||
@ -8,17 +11,17 @@ struct Request {
|
||||
address client;
|
||||
Ask ask;
|
||||
Content content;
|
||||
uint64 expiry; // amount of seconds since start of the request at which this request expires
|
||||
Duration expiry; // amount of seconds since start of the request at which this request expires
|
||||
bytes32 nonce; // random nonce to differentiate between similar requests
|
||||
}
|
||||
|
||||
struct Ask {
|
||||
uint256 proofProbability; // how often storage proofs are required
|
||||
uint256 pricePerBytePerSecond; // amount of tokens paid per second per byte to hosts
|
||||
uint256 collateralPerByte; // amount of tokens per byte required to be deposited by the hosts in order to fill the slot
|
||||
TokensPerSecond pricePerBytePerSecond; // amount of tokens paid per second per byte to hosts
|
||||
uint128 collateralPerByte; // amount of tokens per byte required to be deposited by the hosts in order to fill the slot
|
||||
uint64 slots; // the number of requested slots
|
||||
uint64 slotSize; // amount of storage per slot (in number of bytes)
|
||||
uint64 duration; // how long content should be stored (in seconds)
|
||||
Duration duration; // how long content should be stored (in seconds)
|
||||
uint64 maxSlotLoss; // Max slots that can be lost without data considered to be lost
|
||||
}
|
||||
|
||||
@ -40,20 +43,29 @@ enum SlotState {
|
||||
Filled, // host has filled slot
|
||||
Finished, // successfully completed
|
||||
Failed, // the request has failed
|
||||
Paid, // host has been paid
|
||||
Cancelled, // when request was cancelled then slot is cancelled as well
|
||||
Repair // when slot slot was forcible freed (host was kicked out from hosting the slot because of too many missed proofs) and needs to be repaired
|
||||
}
|
||||
|
||||
library AskHelpers {
|
||||
function collateralPerSlot(Ask memory ask) internal pure returns (uint256) {
|
||||
return ask.collateralPerByte * ask.slotSize;
|
||||
}
|
||||
using AskHelpers for Ask;
|
||||
|
||||
function pricePerSlotPerSecond(
|
||||
Ask memory ask
|
||||
) internal pure returns (uint256) {
|
||||
return ask.pricePerBytePerSecond * ask.slotSize;
|
||||
) internal pure returns (TokensPerSecond) {
|
||||
uint96 perByte = TokensPerSecond.unwrap(ask.pricePerBytePerSecond);
|
||||
return TokensPerSecond.wrap(perByte * ask.slotSize);
|
||||
}
|
||||
|
||||
function pricePerSecond(
|
||||
Ask memory ask
|
||||
) internal pure returns (TokensPerSecond) {
|
||||
uint96 perSlot = TokensPerSecond.unwrap(ask.pricePerSlotPerSecond());
|
||||
return TokensPerSecond.wrap(perSlot * ask.slots);
|
||||
}
|
||||
|
||||
function collateralPerSlot(Ask memory ask) internal pure returns (uint128) {
|
||||
return ask.collateralPerByte * ask.slotSize;
|
||||
}
|
||||
}
|
||||
|
||||
@ -88,11 +100,4 @@ library Requests {
|
||||
result := ids
|
||||
}
|
||||
}
|
||||
|
||||
function maxPrice(Request memory request) internal pure returns (uint256) {
|
||||
return
|
||||
request.ask.slots *
|
||||
request.ask.duration *
|
||||
request.ask.pricePerSlotPerSecond();
|
||||
}
|
||||
}
|
||||
|
||||
@ -5,20 +5,20 @@ import "./Marketplace.sol";
|
||||
|
||||
// exposes internal functions of Marketplace for testing
|
||||
contract TestMarketplace is Marketplace {
|
||||
using VaultHelpers for RequestId;
|
||||
using VaultHelpers for Vault;
|
||||
|
||||
constructor(
|
||||
MarketplaceConfig memory config,
|
||||
IERC20 token,
|
||||
Vault vault,
|
||||
IGroth16Verifier verifier
|
||||
)
|
||||
Marketplace(config, token, verifier) // solhint-disable-next-line no-empty-blocks
|
||||
{}
|
||||
) Marketplace(config, vault, verifier) {}
|
||||
|
||||
function forciblyFreeSlot(SlotId slotId) public {
|
||||
_forciblyFreeSlot(slotId);
|
||||
}
|
||||
|
||||
function getSlotCollateral(SlotId slotId) public view returns (uint256) {
|
||||
return _slots[slotId].currentCollateral;
|
||||
function getSlotBalance(SlotId slotId) public view returns (uint256) {
|
||||
Slot storage slot = _slots[slotId];
|
||||
FundId fund = slot.requestId.asFundId();
|
||||
AccountId account = vault().hostAccount(slot.host, slot.slotIndex);
|
||||
return vault().getBalance(fund, account);
|
||||
}
|
||||
|
||||
function challengeToFieldElement(
|
||||
|
||||
@ -5,8 +5,6 @@ pragma solidity 0.8.28;
|
||||
/// since 1970). Uses a uint40 to facilitate efficient packing in structs. A
|
||||
/// uint40 allows times to be represented for the coming 30 000 years.
|
||||
type Timestamp is uint40;
|
||||
/// Represents a duration of time in seconds
|
||||
type Duration is uint40;
|
||||
|
||||
using {_timestampEquals as ==} for Timestamp global;
|
||||
using {_timestampNotEqual as !=} for Timestamp global;
|
||||
@ -29,12 +27,39 @@ function _timestampAtMost(Timestamp a, Timestamp b) pure returns (bool) {
|
||||
return Timestamp.unwrap(a) <= Timestamp.unwrap(b);
|
||||
}
|
||||
|
||||
/// Represents a duration of time in seconds
|
||||
type Duration is uint40;
|
||||
|
||||
using {_durationEquals as ==} for Duration global;
|
||||
using {_durationGreaterThan as >} for Duration global;
|
||||
using {_durationAtLeast as >=} for Duration global;
|
||||
|
||||
function _durationEquals(Duration a, Duration b) pure returns (bool) {
|
||||
return Duration.unwrap(a) == Duration.unwrap(b);
|
||||
}
|
||||
|
||||
function _durationGreaterThan(Duration a, Duration b) pure returns (bool) {
|
||||
return Duration.unwrap(a) > Duration.unwrap(b);
|
||||
}
|
||||
|
||||
function _durationAtLeast(Duration a, Duration b) pure returns (bool) {
|
||||
return Duration.unwrap(a) >= Duration.unwrap(b);
|
||||
}
|
||||
|
||||
library Timestamps {
|
||||
/// Returns the current block timestamp converted to a Timestamp type
|
||||
function currentTime() internal view returns (Timestamp) {
|
||||
return Timestamp.wrap(uint40(block.timestamp));
|
||||
}
|
||||
|
||||
// Adds a duration to a timestamp
|
||||
function add(
|
||||
Timestamp begin,
|
||||
Duration duration
|
||||
) internal pure returns (Timestamp) {
|
||||
return Timestamp.wrap(Timestamp.unwrap(begin) + Duration.unwrap(duration));
|
||||
}
|
||||
|
||||
/// Calculates the duration from start until end
|
||||
function until(
|
||||
Timestamp start,
|
||||
@ -43,7 +43,7 @@ function _tokensPerSecondAtMost(
|
||||
return TokensPerSecond.unwrap(a) <= TokensPerSecond.unwrap(b);
|
||||
}
|
||||
|
||||
library TokenFlows {
|
||||
library Tokens {
|
||||
/// Calculates how many tokens are accumulated when a token flow is maintained
|
||||
/// for a duration of time.
|
||||
function accumulate(
|
||||
@ -50,6 +50,10 @@ import "./vault/VaultBase.sol";
|
||||
contract Vault is VaultBase, Pausable, Ownable {
|
||||
constructor(IERC20 token) VaultBase(token) Ownable(msg.sender) {}
|
||||
|
||||
function getToken() public view returns (IERC20) {
|
||||
return _token;
|
||||
}
|
||||
|
||||
/// Creates an account id that encodes the address of the account holder, and
|
||||
/// a discriminator. The discriminator can be used to create different
|
||||
/// accounts within a fund that all belong to the same account holder.
|
||||
|
||||
63
contracts/marketplace/Collateral.sol
Normal file
63
contracts/marketplace/Collateral.sol
Normal file
@ -0,0 +1,63 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
pragma solidity 0.8.28;
|
||||
|
||||
import "../Configuration.sol";
|
||||
import "../Requests.sol";
|
||||
|
||||
library Collateral {
|
||||
using Collateral for Request;
|
||||
using Collateral for CollateralConfig;
|
||||
|
||||
function checkCorrectness(
|
||||
CollateralConfig memory configuration
|
||||
) internal pure {
|
||||
require(
|
||||
configuration.repairRewardPercentage <= 100,
|
||||
Marketplace_RepairRewardPercentageTooHigh()
|
||||
);
|
||||
require(
|
||||
configuration.slashPercentage <= 100,
|
||||
Marketplace_SlashPercentageTooHigh()
|
||||
);
|
||||
require(
|
||||
configuration.maxNumberOfSlashes * configuration.slashPercentage <= 100,
|
||||
Marketplace_MaximumSlashingTooHigh()
|
||||
);
|
||||
}
|
||||
|
||||
function slashAmount(
|
||||
CollateralConfig storage configuration,
|
||||
uint128 collateral
|
||||
) internal view returns (uint128) {
|
||||
return (collateral * configuration.slashPercentage) / 100;
|
||||
}
|
||||
|
||||
function repairReward(
|
||||
CollateralConfig storage configuration,
|
||||
uint128 collateral
|
||||
) internal view returns (uint128) {
|
||||
return (collateral * configuration.repairRewardPercentage) / 100;
|
||||
}
|
||||
|
||||
function validatorReward(
|
||||
CollateralConfig storage configuration,
|
||||
uint128 slashed
|
||||
) internal view returns (uint128) {
|
||||
return (slashed * configuration.validatorRewardPercentage) / 100;
|
||||
}
|
||||
|
||||
function designatedCollateral(
|
||||
CollateralConfig storage configuration,
|
||||
uint128 collateral
|
||||
) internal view returns (uint128) {
|
||||
uint8 slashes = configuration.maxNumberOfSlashes;
|
||||
uint128 slashing = configuration.slashAmount(collateral);
|
||||
uint128 validation = slashes * configuration.validatorReward(slashing);
|
||||
uint128 repair = configuration.repairReward(collateral);
|
||||
return collateral - validation - repair;
|
||||
}
|
||||
|
||||
error Marketplace_RepairRewardPercentageTooHigh();
|
||||
error Marketplace_SlashPercentageTooHigh();
|
||||
error Marketplace_MaximumSlashingTooHigh();
|
||||
}
|
||||
44
contracts/marketplace/VaultHelpers.sol
Normal file
44
contracts/marketplace/VaultHelpers.sol
Normal file
@ -0,0 +1,44 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
pragma solidity 0.8.28;
|
||||
|
||||
import "../Requests.sol";
|
||||
import "../Vault.sol";
|
||||
|
||||
library VaultHelpers {
|
||||
enum VaultRole {
|
||||
client,
|
||||
host,
|
||||
validator
|
||||
}
|
||||
|
||||
function clientAccount(
|
||||
Vault vault,
|
||||
address client
|
||||
) internal pure returns (AccountId) {
|
||||
bytes12 discriminator = bytes12(bytes1(uint8(VaultRole.client)));
|
||||
return vault.encodeAccountId(client, discriminator);
|
||||
}
|
||||
|
||||
function hostAccount(
|
||||
Vault vault,
|
||||
address host,
|
||||
uint64 slotIndex
|
||||
) internal pure returns (AccountId) {
|
||||
bytes12 role = bytes12(bytes1(uint8(VaultRole.host)));
|
||||
bytes12 index = bytes12(uint96(slotIndex));
|
||||
bytes12 discriminator = role | index;
|
||||
return vault.encodeAccountId(host, discriminator);
|
||||
}
|
||||
|
||||
function validatorAccount(
|
||||
Vault vault,
|
||||
address validator
|
||||
) internal pure returns (AccountId) {
|
||||
bytes12 discriminator = bytes12(bytes1(uint8(VaultRole.validator)));
|
||||
return vault.encodeAccountId(validator, discriminator);
|
||||
}
|
||||
|
||||
function asFundId(RequestId requestId) internal pure returns (FundId) {
|
||||
return FundId.wrap(RequestId.unwrap(requestId));
|
||||
}
|
||||
}
|
||||
@ -1,8 +1,8 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
pragma solidity 0.8.28;
|
||||
|
||||
import "./TokenFlows.sol";
|
||||
import "./Timestamps.sol";
|
||||
import "../Tokens.sol";
|
||||
import "../Timestamps.sol";
|
||||
|
||||
/// Used to identify an account. The first 20 bytes consist of the address of
|
||||
/// the account holder, and the last 12 bytes consist of a discriminator value.
|
||||
@ -38,7 +38,7 @@ struct Flow {
|
||||
|
||||
library Accounts {
|
||||
using Accounts for Account;
|
||||
using TokenFlows for TokensPerSecond;
|
||||
using Tokens for TokensPerSecond;
|
||||
using Timestamps for Timestamp;
|
||||
|
||||
/// Creates an account id from the account holder address and a discriminator.
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
pragma solidity 0.8.28;
|
||||
|
||||
import "./Timestamps.sol";
|
||||
import "../Timestamps.sol";
|
||||
|
||||
struct Fund {
|
||||
/// The time-lock unlocks at this time
|
||||
|
||||
@ -6,6 +6,9 @@ import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
|
||||
import "./Accounts.sol";
|
||||
import "./Funds.sol";
|
||||
|
||||
/// Unique identifier for a fund, chosen by the controller
|
||||
type FundId is bytes32;
|
||||
|
||||
/// Records account balances and token flows. Accounts are separated into funds.
|
||||
/// Funds are kept separate between controllers.
|
||||
///
|
||||
@ -46,8 +49,6 @@ abstract contract VaultBase {
|
||||
|
||||
/// Represents a smart contract that can redistribute and burn tokens in funds
|
||||
type Controller is address;
|
||||
/// Unique identifier for a fund, chosen by the controller
|
||||
type FundId is bytes32;
|
||||
|
||||
/// Each controller has its own set of funds
|
||||
mapping(Controller => mapping(FundId => Fund)) private _funds;
|
||||
|
||||
@ -9,12 +9,12 @@ async function mine256blocks({ network, ethers }) {
|
||||
|
||||
// deploys a marketplace with a real Groth16 verifier
|
||||
async function deployMarketplace({ deployments, getNamedAccounts }) {
|
||||
const token = await deployments.get("TestToken")
|
||||
const vault = await deployments.get("Vault")
|
||||
const verifier = await deployments.get("Groth16Verifier")
|
||||
const zkeyHash = loadZkeyHash(network.name)
|
||||
let configuration = loadConfiguration(network.name)
|
||||
configuration.proofs.zkeyHash = zkeyHash
|
||||
const args = [configuration, token.address, verifier.address]
|
||||
const args = [configuration, vault.address, verifier.address]
|
||||
const { deployer: from } = await getNamedAccounts()
|
||||
const marketplace = await deployments.deploy("Marketplace", { args, from })
|
||||
console.log("Deployed Marketplace with Groth16 Verifier at:")
|
||||
@ -29,12 +29,12 @@ async function deployTestMarketplace({
|
||||
getNamedAccounts,
|
||||
}) {
|
||||
if (network.tags.local) {
|
||||
const token = await deployments.get("TestToken")
|
||||
const vault = await deployments.get("Vault")
|
||||
const verifier = await deployments.get("TestVerifier")
|
||||
const zkeyHash = loadZkeyHash(network.name)
|
||||
let configuration = loadConfiguration(network.name)
|
||||
configuration.proofs.zkeyHash = zkeyHash
|
||||
const args = [configuration, token.address, verifier.address]
|
||||
const args = [configuration, vault.address, verifier.address]
|
||||
const { deployer: from } = await getNamedAccounts()
|
||||
const marketplace = await deployments.deploy("Marketplace", { args, from })
|
||||
console.log("Deployed Marketplace with Test Verifier at:")
|
||||
@ -50,4 +50,4 @@ module.exports = async (environment) => {
|
||||
}
|
||||
|
||||
module.exports.tags = ["Marketplace"]
|
||||
module.exports.dependencies = ["TestToken", "Verifier"]
|
||||
module.exports.dependencies = ["Vault", "Verifier"]
|
||||
|
||||
13
deploy/vault.js
Normal file
13
deploy/vault.js
Normal file
@ -0,0 +1,13 @@
|
||||
async function deployVault({ deployments, getNamedAccounts }) {
|
||||
const token = await deployments.get("TestToken")
|
||||
const args = [token.address]
|
||||
const { deployer: from } = await getNamedAccounts()
|
||||
await deployments.deploy("Vault", { args, from })
|
||||
}
|
||||
|
||||
module.exports = async (environment) => {
|
||||
await deployVault(environment)
|
||||
}
|
||||
|
||||
module.exports.tags = ["Vault"]
|
||||
module.exports.dependencies = ["TestToken"]
|
||||
3
fuzzing/corpus/.gitignore
vendored
3
fuzzing/corpus/.gitignore
vendored
@ -1,3 +0,0 @@
|
||||
*
|
||||
!.gitignore
|
||||
!.keep
|
||||
@ -1,11 +0,0 @@
|
||||
# configure Echidna fuzzing tests
|
||||
|
||||
testMode: "assertion" # check that solidity asserts are never triggered
|
||||
allContracts: true # allow calls to e.g. TestToken in test scenarios
|
||||
format: "text" # disable interactive ui
|
||||
|
||||
# For a longer test run, consider these options:
|
||||
# timeout: 3600 # limit test run to one hour
|
||||
# testLimit: 100000000000 # do not limit the amount of test sequences
|
||||
# stopOnFail: true # stop on first failure
|
||||
# workers: 8 # use more cpu cores
|
||||
@ -1,33 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
set -e
|
||||
|
||||
root=$(cd $(dirname "$0")/.. && pwd)
|
||||
arch=$(arch)
|
||||
|
||||
if command -v echidna; then
|
||||
fuzz () {
|
||||
echidna ${root} \
|
||||
--config ${root}/fuzzing/echidna.yaml \
|
||||
--corpus-dir ${root}/fuzzing/corpus \
|
||||
--crytic-args --ignore-compile \
|
||||
--contract $1
|
||||
}
|
||||
elif [ "${arch}" = "x86_64" ]; then
|
||||
fuzz () {
|
||||
docker run \
|
||||
--rm \
|
||||
-v ${root}:/src ghcr.io/crytic/echidna/echidna \
|
||||
bash -c \
|
||||
"cd /src && echidna . \
|
||||
--config fuzzing/echidna.yaml \
|
||||
--corpus-dir fuzzing/corpus \
|
||||
--crytic-args --ignore-compile \
|
||||
--contract $1"
|
||||
}
|
||||
else
|
||||
echo "Error: echidna not found, and the docker image does not support ${arch}"
|
||||
echo "Please install echidna: https://github.com/crytic/echidna#installation"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
fuzz FuzzMarketplace
|
||||
@ -3,7 +3,6 @@
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"test": "npm run lint && hardhat test",
|
||||
"fuzz": "hardhat compile && fuzzing/fuzz.sh",
|
||||
"start": "hardhat node --export deployment-localhost.json",
|
||||
"compile": "hardhat compile",
|
||||
"format": "prettier --write contracts/*.sol contracts/**/*.sol test/**/*.js",
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -2,4 +2,9 @@ function collateralPerSlot(request) {
|
||||
return request.ask.collateralPerByte * request.ask.slotSize
|
||||
}
|
||||
|
||||
module.exports = { collateralPerSlot }
|
||||
function repairReward(configuration, collateral) {
|
||||
const percentage = configuration.collateral.repairRewardPercentage
|
||||
return Math.round((collateral * percentage) / 100)
|
||||
}
|
||||
|
||||
module.exports = { collateralPerSlot, repairReward }
|
||||
|
||||
@ -2,10 +2,10 @@ const { ethers } = require("hardhat")
|
||||
const { keccak256, defaultAbiCoder } = ethers.utils
|
||||
|
||||
function requestId(request) {
|
||||
const Ask = "tuple(uint256, uint256, uint256, uint64, uint64, uint64, int64)"
|
||||
const Ask = "tuple(uint256, uint96, uint256, uint64, uint64, uint40, uint64)"
|
||||
const Content = "tuple(bytes, bytes32)"
|
||||
const Request =
|
||||
"tuple(address, " + Ask + ", " + Content + ", uint64, bytes32)"
|
||||
"tuple(address, " + Ask + ", " + Content + ", uint40, bytes32)"
|
||||
return keccak256(defaultAbiCoder.encode([Request], requestToArray(request)))
|
||||
}
|
||||
|
||||
|
||||
@ -4,28 +4,32 @@ const { payoutForDuration } = require("./price")
|
||||
const { collateralPerSlot } = require("./collateral")
|
||||
|
||||
async function waitUntilCancelled(contract, request) {
|
||||
const expiry = (await contract.requestExpiry(requestId(request))).toNumber()
|
||||
const expiry = await contract.requestExpiry(requestId(request))
|
||||
// We do +1, because the expiry check in contract is done as `>` and not `>=`.
|
||||
await advanceTimeTo(expiry + 1)
|
||||
}
|
||||
|
||||
async function waitUntilSlotsFilled(contract, request, proof, token, slots) {
|
||||
async function waitUntilSlotFilled(contract, request, proof, token, slotIndex) {
|
||||
let collateral = collateralPerSlot(request)
|
||||
await token.approve(contract.address, collateral * slots.length)
|
||||
await token.approve(contract.address, collateral)
|
||||
await contract.reserveSlot(requestId(request), slotIndex)
|
||||
await contract.fillSlot(requestId(request), slotIndex, proof)
|
||||
const start = await currentTime()
|
||||
const end = await contract.requestEnd(requestId(request))
|
||||
return payoutForDuration(request, start, end)
|
||||
}
|
||||
|
||||
let requestEnd = (await contract.requestEnd(requestId(request))).toNumber()
|
||||
async function waitUntilSlotsFilled(contract, request, proof, token, slots) {
|
||||
const payouts = []
|
||||
for (let slotIndex of slots) {
|
||||
await contract.reserveSlot(requestId(request), slotIndex)
|
||||
await contract.fillSlot(requestId(request), slotIndex, proof)
|
||||
|
||||
payouts[slotIndex] = payoutForDuration(
|
||||
payouts[slotIndex] = await waitUntilSlotFilled(
|
||||
contract,
|
||||
request,
|
||||
await currentTime(),
|
||||
requestEnd
|
||||
proof,
|
||||
token,
|
||||
slotIndex
|
||||
)
|
||||
}
|
||||
|
||||
return payouts
|
||||
}
|
||||
|
||||
@ -40,7 +44,7 @@ async function waitUntilStarted(contract, request, proof, token) {
|
||||
}
|
||||
|
||||
async function waitUntilFinished(contract, requestId) {
|
||||
const end = (await contract.requestEnd(requestId)).toNumber()
|
||||
const end = await contract.requestEnd(requestId)
|
||||
// We do +1, because the end check in contract is done as `>` and not `>=`.
|
||||
await advanceTimeTo(end + 1)
|
||||
}
|
||||
@ -50,7 +54,7 @@ async function waitUntilFailed(contract, request) {
|
||||
for (let i = 0; i <= request.ask.maxSlotLoss; i++) {
|
||||
slot.index = i
|
||||
let id = slotId(slot)
|
||||
await contract.forciblyFreeSlot(id)
|
||||
await contract.freeSlot(id)
|
||||
}
|
||||
}
|
||||
|
||||
@ -59,7 +63,7 @@ async function waitUntilSlotFailed(contract, request, slot) {
|
||||
let freed = 0
|
||||
while (freed <= request.ask.maxSlotLoss) {
|
||||
if (index !== slot.index) {
|
||||
await contract.forciblyFreeSlot(slotId({ ...slot, index }))
|
||||
await contract.freeSlot(slotId({ ...slot, index }))
|
||||
freed++
|
||||
}
|
||||
index++
|
||||
@ -99,6 +103,7 @@ function patchOverloads(contract) {
|
||||
module.exports = {
|
||||
waitUntilCancelled,
|
||||
waitUntilStarted,
|
||||
waitUntilSlotFilled,
|
||||
waitUntilSlotsFilled,
|
||||
waitUntilFinished,
|
||||
waitUntilFailed,
|
||||
|
||||
@ -1,5 +1,4 @@
|
||||
const { Assertion } = require("chai")
|
||||
const { currentTime } = require("./evm")
|
||||
|
||||
const RequestState = {
|
||||
New: 0,
|
||||
@ -14,9 +13,8 @@ const SlotState = {
|
||||
Filled: 1,
|
||||
Finished: 2,
|
||||
Failed: 3,
|
||||
Paid: 4,
|
||||
Cancelled: 5,
|
||||
Repair: 6,
|
||||
Cancelled: 4,
|
||||
Repair: 5,
|
||||
}
|
||||
|
||||
function enableRequestAssertions() {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user