mirror of
https://github.com/logos-storage/logos-storage-contracts-eth.git
synced 2026-01-04 14:23:10 +00:00
* vault: deposit and withdraw * vault: change data structure to be recipient oriented * vault: burning funds * vault: transfer tokens from one recipient to the other * vault: designate tokens for a single recipient * vault: lock up tokens until expiry time * vault: lock is deleted upon withdrawal * vault: simplify test setup * vault: remove duplication in tests * vault: further test for locks * vault: allow recipient to withdraw * vault: flow tokens from one recipient to the other * vault: designate tokens that flow * vault: move flow accumulation calculation into VaultBase * vault: use custom operators to improve readability * vault: stop flowing when lock expires * vault: reject flow when insufficient tokens available * vault: do not allow flow when lock already expired * vault: allow automine to be disabled in time sensitive tests * vault: improve naming of public functions * vault: flow to multiple recipients - changes balance from uint256 -> uint128 so that entire Balance can be read or written with a single operation - moves Lock to library - simplifies lock checks * vault: reject negative flows * vault: make tests a bit more robust * vault: change flows over time * vault: check Lock invariant before writing * vault: allow flows to be diverted to others * vault: simplify example flow rates in test * vault: disallow transfer of flowing tokens * vault: cannot burn flowing tokens * vault: delete flow when burning or withdrawing * vault: fix flaky time sensitive tests Ensures that setting of lock and starting of flow happen in the same block. Therefore hardhat cannot occasionally increase the timestamp between the two operations. This makes predicting the balances over time much easier. * vault: disallow designating of flowing tokens * vault: document setAutomine() * vault: delete lock all tokens are withdrawn or burned * vault: cleanup * vault: reorder tests * vault: only allow deposit, transfer, etc when locked * vault: reorder functions in roughly chronological order * vault: rename context -> fund * vault: rename balance -> account * vault: combine account and flow mappings * vault: _getAccount updates to the latest timestamp * vault: simplify _getAccount() * vault: reordering * vault: formatting * vault: do not delete lock when burning * vault: combine Account and Flow structs * vault: cleanup * vault: split flow into incoming and outgoing - no need to deal with signed integers anymore - allows flow to self to designate tokens over time * vault: fix transfer to self * vault: remove _getAccount() - no longer calculate flow updates when not needed - use account.update(timestamp) where needed - use _getBalance() to view current balance * vault: rename error * vault: reduce size of timestamp further * vault: prevent approval hijacking - transfer ERC20 funds into the vault from the controller, not from the user - prevents an attacker from hijacking a user's ERC20 approval to move tokens into a part of the vault that is controlled by the attacker * vault: extract common tests for unlocked funds * vault: burn entire fund * vault: transfer tokens to 0xdead when fund is burned * vault: do not expose Lock internals on public api * vault: formatting * vault: test lock state transitions * vault: clean up errors * vault: rename burn -> burnAccount, burnAll -> burnFund * vault: burn part of designated tokens * vault: burn designated/fund allowed when flowing * vault: prefix errors with 'Vault' * vault: cleanup * vault: remove dead code * vault: add documentation * vault: fix accounting of locked value when burning designated tokens * vault: update documentation * update openzeppelin contracts to 5.2.0 * vault: format all solidity files * vault: cleanup tests * vault: pausing and unpausing * vault: rename account->holder in tests * vault: allow for multiple accounts for one account holder * vault: only allow account holder to withdraw for itself * vault: freezeFund() instead of burnFund() * vault: rename Fund -> FundId * vault: rename lock states - NoLock -> Inactive - Unlocked -> Withdrawing * vault: rename Lock -> Fund * vault: clarification Co-Authored-by: Adam Uhlíř <adam@uhlir.dev> * vault: rename update() -> accumulateFlows() Reason: update() is too generic, and can easily be interpreted as changing the on-chain state, whereas it actually updates the in-memory struct. Co-Authored-By: Eric <5089238+emizzle@users.noreply.github.com> Co-Authored-By: Adam Uhlíř <adam@uhlir.dev> * vault: rephrase Co-Authored-By: Adam Uhlíř <adam@uhlir.dev> --------- Co-authored-by: Adam Uhlíř <adam@uhlir.dev> Co-authored-by: Eric <5089238+emizzle@users.noreply.github.com>
1601 lines
58 KiB
JavaScript
1601 lines
58 KiB
JavaScript
const { ethers } = require("hardhat")
|
|
const { AddressZero } = ethers.constants
|
|
const { BigNumber } = ethers
|
|
const { expect } = require("chai")
|
|
const {
|
|
exampleConfiguration,
|
|
exampleRequest,
|
|
exampleProof,
|
|
invalidProof,
|
|
} = require("./examples")
|
|
const { periodic, hours } = require("./time")
|
|
const { requestId, slotId, askToArray } = require("./ids")
|
|
const {
|
|
RequestState,
|
|
SlotState,
|
|
enableRequestAssertions,
|
|
} = require("./requests")
|
|
const {
|
|
waitUntilCancelled,
|
|
waitUntilStarted,
|
|
waitUntilFinished,
|
|
waitUntilFailed,
|
|
waitUntilSlotFailed,
|
|
patchOverloads,
|
|
} = require("./marketplace")
|
|
const {
|
|
maxPrice,
|
|
pricePerSlotPerSecond,
|
|
payoutForDuration,
|
|
} = require("./price")
|
|
const { collateralPerSlot } = require("./collateral")
|
|
const {
|
|
snapshot,
|
|
revert,
|
|
ensureMinimumBlockHeight,
|
|
advanceTime,
|
|
advanceTimeTo,
|
|
currentTime,
|
|
setNextBlockTimestamp,
|
|
} = require("./evm")
|
|
const { arrayify } = require("ethers/lib/utils")
|
|
|
|
const ACCOUNT_STARTING_BALANCE = 1_000_000_000_000_000
|
|
|
|
describe("Marketplace constructor", function () {
|
|
let Marketplace, token, verifier, config
|
|
|
|
beforeEach(async function () {
|
|
await snapshot()
|
|
await ensureMinimumBlockHeight(256)
|
|
|
|
const TestToken = await ethers.getContractFactory("TestToken")
|
|
token = await TestToken.deploy()
|
|
|
|
const TestVerifier = await ethers.getContractFactory("TestVerifier")
|
|
verifier = await TestVerifier.deploy()
|
|
|
|
Marketplace = await ethers.getContractFactory("TestMarketplace")
|
|
config = exampleConfiguration()
|
|
})
|
|
|
|
afterEach(async function () {
|
|
await revert()
|
|
})
|
|
|
|
function testPercentageOverflow(property, expectedError) {
|
|
it(`should reject for ${property} overflowing percentage values`, async () => {
|
|
config.collateral[property] = 101
|
|
|
|
await expect(
|
|
Marketplace.deploy(config, token.address, verifier.address)
|
|
).to.be.revertedWith(expectedError)
|
|
})
|
|
}
|
|
|
|
testPercentageOverflow(
|
|
"repairRewardPercentage",
|
|
"Marketplace_RepairRewardPercentageTooHigh"
|
|
)
|
|
testPercentageOverflow(
|
|
"slashPercentage",
|
|
"Marketplace_SlashPercentageTooHigh"
|
|
)
|
|
|
|
it("should reject when total slash percentage exceeds 100%", async () => {
|
|
config.collateral.slashPercentage = 1
|
|
config.collateral.maxNumberOfSlashes = 101
|
|
|
|
await expect(
|
|
Marketplace.deploy(config, token.address, verifier.address)
|
|
).to.be.revertedWith("Marketplace_MaximumSlashingTooHigh")
|
|
})
|
|
})
|
|
|
|
describe("Marketplace", function () {
|
|
const proof = exampleProof()
|
|
const config = exampleConfiguration()
|
|
|
|
let marketplace
|
|
let token
|
|
let verifier
|
|
let client,
|
|
clientWithdrawRecipient,
|
|
host,
|
|
host1,
|
|
host2,
|
|
host3,
|
|
hostRewardRecipient,
|
|
hostCollateralRecipient,
|
|
validatorRecipient
|
|
let request
|
|
let slot
|
|
|
|
enableRequestAssertions()
|
|
|
|
beforeEach(async function () {
|
|
await snapshot()
|
|
await ensureMinimumBlockHeight(256)
|
|
;[
|
|
client,
|
|
clientWithdrawRecipient,
|
|
host1,
|
|
host2,
|
|
host3,
|
|
hostRewardRecipient,
|
|
hostCollateralRecipient,
|
|
validatorRecipient,
|
|
] = await ethers.getSigners()
|
|
host = host1
|
|
|
|
const TestToken = await ethers.getContractFactory("TestToken")
|
|
token = await TestToken.deploy()
|
|
for (let account of [
|
|
client,
|
|
clientWithdrawRecipient,
|
|
host1,
|
|
host2,
|
|
host3,
|
|
hostRewardRecipient,
|
|
hostCollateralRecipient,
|
|
validatorRecipient,
|
|
]) {
|
|
await token.mint(account.address, ACCOUNT_STARTING_BALANCE)
|
|
}
|
|
|
|
const TestVerifier = await ethers.getContractFactory("TestVerifier")
|
|
verifier = await TestVerifier.deploy()
|
|
|
|
const Marketplace = await ethers.getContractFactory("TestMarketplace")
|
|
marketplace = await Marketplace.deploy(
|
|
config,
|
|
token.address,
|
|
verifier.address
|
|
)
|
|
patchOverloads(marketplace)
|
|
|
|
request = await exampleRequest()
|
|
request.client = client.address
|
|
|
|
slot = {
|
|
request: requestId(request),
|
|
index: request.ask.slots / 2,
|
|
}
|
|
})
|
|
|
|
afterEach(async function () {
|
|
await revert()
|
|
})
|
|
|
|
function switchAccount(account) {
|
|
token = token.connect(account)
|
|
marketplace = marketplace.connect(account)
|
|
patchOverloads(marketplace)
|
|
}
|
|
|
|
describe("requesting storage", function () {
|
|
beforeEach(function () {
|
|
switchAccount(client)
|
|
})
|
|
|
|
it("emits event when storage is requested", async function () {
|
|
await token.approve(marketplace.address, maxPrice(request))
|
|
const now = await currentTime()
|
|
await setNextBlockTimestamp(now)
|
|
const expectedExpiry = now + request.expiry
|
|
await expect(marketplace.requestStorage(request))
|
|
.to.emit(marketplace, "StorageRequested")
|
|
.withArgs(requestId(request), askToArray(request.ask), expectedExpiry)
|
|
})
|
|
|
|
it("allows retrieval of request details", async function () {
|
|
await token.approve(marketplace.address, maxPrice(request))
|
|
await marketplace.requestStorage(request)
|
|
const id = requestId(request)
|
|
expect(await marketplace.getRequest(id)).to.be.request(request)
|
|
})
|
|
|
|
it("rejects request with invalid client address", async function () {
|
|
let invalid = { ...request, client: host.address }
|
|
await token.approve(marketplace.address, maxPrice(invalid))
|
|
await expect(marketplace.requestStorage(invalid)).to.be.revertedWith(
|
|
"Marketplace_InvalidClientAddress"
|
|
)
|
|
})
|
|
|
|
it("rejects request with duration exceeding limit", async function () {
|
|
request.ask.duration = config.requestDurationLimit + 1
|
|
await token.approve(marketplace.address, collateralPerSlot(request))
|
|
await expect(marketplace.requestStorage(request)).to.be.revertedWith(
|
|
"Marketplace_DurationExceedsLimit"
|
|
)
|
|
})
|
|
|
|
it("rejects request with insufficient payment", async function () {
|
|
let insufficient = maxPrice(request) - 1
|
|
await token.approve(marketplace.address, insufficient)
|
|
await expect(marketplace.requestStorage(request)).to.be.revertedWith(
|
|
"ERC20InsufficientAllowance"
|
|
)
|
|
})
|
|
|
|
it("rejects request when expiry out of bounds", async function () {
|
|
await token.approve(marketplace.address, maxPrice(request))
|
|
|
|
request.expiry = request.ask.duration + 1
|
|
await expect(marketplace.requestStorage(request)).to.be.revertedWith(
|
|
"Marketplace_InvalidExpiry"
|
|
)
|
|
|
|
request.expiry = 0
|
|
await expect(marketplace.requestStorage(request)).to.be.revertedWith(
|
|
"Marketplace_InvalidExpiry"
|
|
)
|
|
})
|
|
|
|
it("is rejected with insufficient slots ", async function () {
|
|
request.ask.slots = 0
|
|
await expect(marketplace.requestStorage(request)).to.be.revertedWith(
|
|
"Marketplace_InsufficientSlots"
|
|
)
|
|
})
|
|
|
|
it("is rejected when maxSlotLoss exceeds slots", async function () {
|
|
request.ask.maxSlotLoss = request.ask.slots + 1
|
|
await expect(marketplace.requestStorage(request)).to.be.revertedWith(
|
|
"Marketplace_InvalidMaxSlotLoss"
|
|
)
|
|
})
|
|
|
|
it("rejects resubmission of request", async function () {
|
|
await token.approve(marketplace.address, maxPrice(request) * 2)
|
|
await marketplace.requestStorage(request)
|
|
await expect(marketplace.requestStorage(request)).to.be.revertedWith(
|
|
"Marketplace_RequestAlreadyExists"
|
|
)
|
|
})
|
|
|
|
it("is rejected when insufficient duration", async function () {
|
|
request.ask.duration = 0
|
|
await expect(marketplace.requestStorage(request)).to.be.revertedWith(
|
|
// request.expiry has to be > 0 and
|
|
// request.expiry < request.ask.duration
|
|
// so request.ask.duration will trigger "Marketplace_InvalidExpiry"
|
|
"Marketplace_InvalidExpiry"
|
|
)
|
|
})
|
|
|
|
it("is rejected when insufficient proofProbability", async function () {
|
|
request.ask.proofProbability = 0
|
|
await expect(marketplace.requestStorage(request)).to.be.revertedWith(
|
|
"Marketplace_InsufficientProofProbability"
|
|
)
|
|
})
|
|
|
|
it("is rejected when insufficient collateral", async function () {
|
|
request.ask.collateralPerByte = 0
|
|
await expect(marketplace.requestStorage(request)).to.be.revertedWith(
|
|
"Marketplace_InsufficientCollateral"
|
|
)
|
|
})
|
|
|
|
it("is rejected when insufficient reward", async function () {
|
|
request.ask.pricePerBytePerSecond = 0
|
|
await expect(marketplace.requestStorage(request)).to.be.revertedWith(
|
|
"Marketplace_InsufficientReward"
|
|
)
|
|
})
|
|
|
|
it("is rejected when cid is missing", async function () {
|
|
request.content.cid = []
|
|
await expect(marketplace.requestStorage(request)).to.be.revertedWith(
|
|
"Marketplace_InvalidCid"
|
|
)
|
|
})
|
|
})
|
|
|
|
describe("filling a slot with collateral", function () {
|
|
beforeEach(async function () {
|
|
switchAccount(client)
|
|
await token.approve(marketplace.address, maxPrice(request))
|
|
await marketplace.requestStorage(request)
|
|
switchAccount(host)
|
|
await token.approve(marketplace.address, collateralPerSlot(request))
|
|
})
|
|
|
|
it("emits event when slot is filled", async function () {
|
|
await marketplace.reserveSlot(slot.request, slot.index)
|
|
await expect(marketplace.fillSlot(slot.request, slot.index, proof))
|
|
.to.emit(marketplace, "SlotFilled")
|
|
.withArgs(slot.request, slot.index)
|
|
})
|
|
|
|
it("allows retrieval of host that filled slot", async function () {
|
|
expect(await marketplace.getHost(slotId(slot))).to.equal(AddressZero)
|
|
await marketplace.reserveSlot(slot.request, slot.index)
|
|
await marketplace.fillSlot(slot.request, slot.index, proof)
|
|
expect(await marketplace.getHost(slotId(slot))).to.equal(host.address)
|
|
})
|
|
|
|
it("gives discount on the collateral for repaired slot", async function () {
|
|
await marketplace.reserveSlot(slot.request, slot.index)
|
|
await marketplace.fillSlot(slot.request, slot.index, proof)
|
|
await marketplace.freeSlot(slotId(slot))
|
|
expect(await marketplace.slotState(slotId(slot))).to.equal(
|
|
SlotState.Repair
|
|
)
|
|
|
|
// We need to advance the time to next period, because filling slot
|
|
// must not be done in the same period as for that period there was already proof
|
|
// submitted with the previous `fillSlot` and the transaction would revert with "Proof already submitted".
|
|
await advanceTime(config.proofs.period + 1)
|
|
|
|
const startBalance = await token.balanceOf(host.address)
|
|
const collateral = collateralPerSlot(request)
|
|
const discountedCollateral =
|
|
collateral -
|
|
Math.round(
|
|
(collateral * config.collateral.repairRewardPercentage) / 100
|
|
)
|
|
await token.approve(marketplace.address, discountedCollateral)
|
|
await marketplace.fillSlot(slot.request, slot.index, proof)
|
|
const endBalance = await token.balanceOf(host.address)
|
|
expect(startBalance - endBalance).to.equal(discountedCollateral)
|
|
expect(await marketplace.slotState(slotId(slot))).to.equal(
|
|
SlotState.Filled
|
|
)
|
|
})
|
|
|
|
it("fails to retrieve a request of an empty slot", async function () {
|
|
expect(marketplace.getActiveSlot(slotId(slot))).to.be.revertedWith(
|
|
"Marketplace_SlotIsFree"
|
|
)
|
|
})
|
|
|
|
it("allows retrieval of request of a filled slot", async function () {
|
|
await marketplace.reserveSlot(slot.request, slot.index)
|
|
await marketplace.fillSlot(slot.request, slot.index, proof)
|
|
let activeSlot = await marketplace.getActiveSlot(slotId(slot))
|
|
expect(activeSlot.request).to.be.request(request)
|
|
expect(activeSlot.slotIndex).to.equal(slot.index)
|
|
})
|
|
|
|
it("is rejected when proof is incorrect", async function () {
|
|
await marketplace.reserveSlot(slot.request, slot.index)
|
|
await expect(
|
|
marketplace.fillSlot(slot.request, slot.index, invalidProof())
|
|
).to.be.revertedWith("Proofs_InvalidProof")
|
|
})
|
|
|
|
it("is rejected when slot already filled", async function () {
|
|
await marketplace.reserveSlot(slot.request, slot.index)
|
|
await marketplace.fillSlot(slot.request, slot.index, proof)
|
|
await expect(
|
|
marketplace.fillSlot(slot.request, slot.index, proof)
|
|
).to.be.revertedWith("Marketplace_SlotNotFree")
|
|
})
|
|
|
|
it("is rejected when request is unknown", async function () {
|
|
let unknown = await exampleRequest()
|
|
await expect(
|
|
marketplace.fillSlot(requestId(unknown), 0, proof)
|
|
).to.be.revertedWith("Marketplace_UnknownRequest")
|
|
})
|
|
|
|
it("is rejected when request is cancelled", async function () {
|
|
switchAccount(client)
|
|
let expired = { ...request, expiry: hours(1) + 1 }
|
|
await token.approve(marketplace.address, maxPrice(request))
|
|
await marketplace.requestStorage(expired)
|
|
await waitUntilCancelled(marketplace, expired)
|
|
switchAccount(host)
|
|
await marketplace.reserveSlot(requestId(expired), slot.index)
|
|
await expect(
|
|
marketplace.fillSlot(requestId(expired), slot.index, proof)
|
|
).to.be.revertedWith("Marketplace_SlotNotFree")
|
|
})
|
|
|
|
it("is rejected when request is finished", async function () {
|
|
await waitUntilStarted(marketplace, request, proof, token)
|
|
await waitUntilFinished(marketplace, slot.request)
|
|
await expect(
|
|
marketplace.fillSlot(slot.request, slot.index, proof)
|
|
).to.be.revertedWith("Marketplace_SlotNotFree")
|
|
})
|
|
|
|
it("is rejected when request is failed", async function () {
|
|
await waitUntilStarted(marketplace, request, proof, token)
|
|
await waitUntilFailed(marketplace, request)
|
|
await expect(
|
|
marketplace.fillSlot(slot.request, slot.index, proof)
|
|
).to.be.revertedWith("Marketplace_SlotNotFree")
|
|
})
|
|
|
|
it("is rejected when slot index not in range", async function () {
|
|
const invalid = request.ask.slots
|
|
await expect(
|
|
marketplace.fillSlot(slot.request, invalid, proof)
|
|
).to.be.revertedWith("Marketplace_InvalidSlot")
|
|
})
|
|
|
|
it("fails when all slots are already filled", async function () {
|
|
const lastSlot = request.ask.slots - 1
|
|
await token.approve(
|
|
marketplace.address,
|
|
collateralPerSlot(request) * lastSlot
|
|
)
|
|
await token.approve(marketplace.address, maxPrice(request) * lastSlot)
|
|
for (let i = 0; i <= lastSlot; i++) {
|
|
await marketplace.reserveSlot(slot.request, i)
|
|
await marketplace.fillSlot(slot.request, i, proof)
|
|
}
|
|
await expect(
|
|
marketplace.fillSlot(slot.request, lastSlot, proof)
|
|
).to.be.revertedWith("Marketplace_SlotNotFree")
|
|
})
|
|
|
|
it("fails if slot is not reserved first", async function () {
|
|
await expect(
|
|
marketplace.fillSlot(slot.request, slot.index, proof)
|
|
).to.be.revertedWith("Marketplace_ReservationRequired")
|
|
})
|
|
})
|
|
|
|
describe("filling slot without collateral", function () {
|
|
beforeEach(async function () {
|
|
switchAccount(client)
|
|
await token.approve(marketplace.address, maxPrice(request))
|
|
await marketplace.requestStorage(request)
|
|
switchAccount(host)
|
|
})
|
|
|
|
it("is rejected when approved collateral is insufficient", async function () {
|
|
let insufficient = collateralPerSlot(request) - 1
|
|
await token.approve(marketplace.address, insufficient)
|
|
await marketplace.reserveSlot(slot.request, slot.index)
|
|
await expect(
|
|
marketplace.fillSlot(slot.request, slot.index, proof)
|
|
).to.be.revertedWith("ERC20InsufficientAllowance")
|
|
})
|
|
|
|
it("collects only requested collateral and not more", async function () {
|
|
await token.approve(marketplace.address, collateralPerSlot(request) * 2)
|
|
const startBalance = await token.balanceOf(host.address)
|
|
await marketplace.reserveSlot(slot.request, slot.index)
|
|
await marketplace.fillSlot(slot.request, slot.index, proof)
|
|
const endBalance = await token.balanceOf(host.address)
|
|
expect(startBalance - endBalance).to.eq(collateralPerSlot(request))
|
|
})
|
|
})
|
|
|
|
describe("submitting proofs when slot is filled", function () {
|
|
beforeEach(async function () {
|
|
switchAccount(client)
|
|
await token.approve(marketplace.address, maxPrice(request))
|
|
await marketplace.requestStorage(request)
|
|
switchAccount(host)
|
|
const collateral = collateralPerSlot(request)
|
|
await token.approve(marketplace.address, collateral)
|
|
await marketplace.reserveSlot(slot.request, slot.index)
|
|
await marketplace.fillSlot(slot.request, slot.index, proof)
|
|
await advanceTime(config.proofs.period)
|
|
})
|
|
|
|
it("allows proofs to be submitted", async function () {
|
|
await marketplace.submitProof(slotId(slot), proof)
|
|
})
|
|
|
|
it("reverts when somebody other then host submit the proof", async function () {
|
|
switchAccount(host2)
|
|
await expect(
|
|
marketplace.submitProof(slotId(slot), proof)
|
|
).to.be.revertedWith("Marketplace_ProofNotSubmittedByHost")
|
|
})
|
|
|
|
it("converts first 31 bytes of challenge to field element", async function () {
|
|
let challenge = arrayify(await marketplace.getChallenge(slotId(slot)))
|
|
let truncated = challenge.slice(0, 31)
|
|
let littleEndian = new Uint8Array(truncated).reverse()
|
|
let expected = BigNumber.from(littleEndian)
|
|
expect(await marketplace.challengeToFieldElement(challenge)).to.equal(
|
|
expected
|
|
)
|
|
})
|
|
|
|
it("converts merkle root to field element", async function () {
|
|
let merkleRoot = request.content.merkleRoot
|
|
let littleEndian = new Uint8Array(merkleRoot).reverse()
|
|
let expected = BigNumber.from(littleEndian)
|
|
expect(await marketplace.merkleRootToFieldElement(merkleRoot)).to.equal(
|
|
expected
|
|
)
|
|
})
|
|
})
|
|
|
|
describe("request end", function () {
|
|
var requestTime
|
|
beforeEach(async function () {
|
|
switchAccount(client)
|
|
await token.approve(marketplace.address, maxPrice(request))
|
|
await marketplace.requestStorage(request)
|
|
requestTime = await currentTime()
|
|
switchAccount(host)
|
|
const collateral = collateralPerSlot(request)
|
|
await token.approve(marketplace.address, collateral)
|
|
})
|
|
|
|
it("sets the request end time to now + duration", async function () {
|
|
await marketplace.reserveSlot(slot.request, slot.index)
|
|
await marketplace.fillSlot(slot.request, slot.index, proof)
|
|
await expect(
|
|
(await marketplace.requestEnd(requestId(request))).toNumber()
|
|
).to.equal(requestTime + request.ask.duration)
|
|
})
|
|
|
|
it("sets request end time to the past once failed", async function () {
|
|
await waitUntilStarted(marketplace, request, proof, token)
|
|
await waitUntilFailed(marketplace, request)
|
|
const now = await currentTime()
|
|
await expect(await marketplace.requestEnd(requestId(request))).to.be.eq(
|
|
now - 1
|
|
)
|
|
})
|
|
|
|
it("sets request end time to the past once cancelled", async function () {
|
|
await marketplace.reserveSlot(slot.request, slot.index)
|
|
await marketplace.fillSlot(slot.request, slot.index, proof)
|
|
await waitUntilCancelled(marketplace, request)
|
|
const now = await currentTime()
|
|
await expect(await marketplace.requestEnd(requestId(request))).to.be.eq(
|
|
now - 1
|
|
)
|
|
})
|
|
|
|
it("checks that request end time is in the past once finished", async function () {
|
|
await waitUntilStarted(marketplace, request, proof, token)
|
|
await waitUntilFinished(marketplace, requestId(request))
|
|
const now = await currentTime()
|
|
await expect(await marketplace.requestEnd(requestId(request))).to.be.eq(
|
|
now - 1
|
|
)
|
|
})
|
|
})
|
|
|
|
describe("freeing a slot", function () {
|
|
let id
|
|
|
|
beforeEach(async function () {
|
|
slot.index = 0
|
|
id = slotId(slot)
|
|
|
|
switchAccount(client)
|
|
await token.approve(marketplace.address, maxPrice(request))
|
|
await marketplace.requestStorage(request)
|
|
switchAccount(host)
|
|
const collateral = collateralPerSlot(request)
|
|
await token.approve(marketplace.address, collateral)
|
|
})
|
|
|
|
it("fails to free slot when slot not filled", async function () {
|
|
slot.index = 5
|
|
let nonExistentId = slotId(slot)
|
|
await expect(marketplace.freeSlot(nonExistentId)).to.be.revertedWith(
|
|
"Marketplace_SlotIsFree"
|
|
)
|
|
})
|
|
|
|
it("can only be freed by the host occupying the slot", async function () {
|
|
await waitUntilStarted(marketplace, request, proof, token)
|
|
switchAccount(client)
|
|
await expect(marketplace.freeSlot(id)).to.be.revertedWith(
|
|
"Marketplace_InvalidSlotHost"
|
|
)
|
|
})
|
|
|
|
it("successfully frees slot", async function () {
|
|
await waitUntilStarted(marketplace, request, proof, token)
|
|
await expect(marketplace.freeSlot(id)).not.to.be.reverted
|
|
})
|
|
|
|
it("emits event once slot is freed", async function () {
|
|
await waitUntilStarted(marketplace, request, proof, token)
|
|
await expect(await marketplace.freeSlot(id))
|
|
.to.emit(marketplace, "SlotFreed")
|
|
.withArgs(slot.request, slot.index)
|
|
})
|
|
})
|
|
|
|
describe("paying out a slot", function () {
|
|
beforeEach(async function () {
|
|
switchAccount(client)
|
|
await token.approve(marketplace.address, maxPrice(request))
|
|
await marketplace.requestStorage(request)
|
|
switchAccount(host)
|
|
const collateral = collateralPerSlot(request)
|
|
await token.approve(marketplace.address, collateral)
|
|
})
|
|
|
|
it("finished request pays out reward based on time hosted", async function () {
|
|
// We are advancing the time because most of the slots will be filled somewhere
|
|
// in the "expiry window" and not at its beginning. This is more "real" setup
|
|
// and demonstrates the partial payout feature better.
|
|
await advanceTime(request.expiry / 2)
|
|
|
|
const expectedPayouts = await waitUntilStarted(
|
|
marketplace,
|
|
request,
|
|
proof,
|
|
token
|
|
)
|
|
await waitUntilFinished(marketplace, requestId(request))
|
|
|
|
const startBalanceHost = await token.balanceOf(host.address)
|
|
await marketplace.freeSlot(slotId(slot))
|
|
const endBalanceHost = await token.balanceOf(host.address)
|
|
|
|
expect(expectedPayouts[slot.index]).to.be.lt(maxPrice(request))
|
|
const collateral = collateralPerSlot(request)
|
|
expect(endBalanceHost - startBalanceHost).to.equal(
|
|
expectedPayouts[slot.index] + collateral
|
|
)
|
|
})
|
|
|
|
it("returns collateral to host collateral address if specified", async function () {
|
|
await waitUntilStarted(marketplace, request, proof, token)
|
|
await waitUntilFinished(marketplace, requestId(request))
|
|
|
|
const startBalanceHost = await token.balanceOf(host.address)
|
|
const startBalanceCollateral = await token.balanceOf(
|
|
hostCollateralRecipient.address
|
|
)
|
|
|
|
const collateralToBeReturned = await marketplace.currentCollateral(
|
|
slotId(slot)
|
|
)
|
|
|
|
await marketplace.freeSlot(
|
|
slotId(slot),
|
|
hostRewardRecipient.address,
|
|
hostCollateralRecipient.address
|
|
)
|
|
|
|
const endBalanceCollateral = await token.balanceOf(
|
|
hostCollateralRecipient.address
|
|
)
|
|
|
|
const endBalanceHost = await token.balanceOf(host.address)
|
|
expect(endBalanceHost).to.equal(startBalanceHost)
|
|
expect(endBalanceCollateral - startBalanceCollateral).to.equal(
|
|
collateralPerSlot(request)
|
|
)
|
|
expect(collateralToBeReturned).to.equal(collateralPerSlot(request))
|
|
})
|
|
|
|
it("pays reward to host reward address if specified", async function () {
|
|
await waitUntilStarted(marketplace, request, proof, token)
|
|
await waitUntilFinished(marketplace, requestId(request))
|
|
|
|
const startBalanceHost = await token.balanceOf(host.address)
|
|
const startBalanceReward = await token.balanceOf(
|
|
hostRewardRecipient.address
|
|
)
|
|
|
|
await marketplace.freeSlot(
|
|
slotId(slot),
|
|
hostRewardRecipient.address,
|
|
hostCollateralRecipient.address
|
|
)
|
|
|
|
const endBalanceHost = await token.balanceOf(host.address)
|
|
const endBalanceReward = await token.balanceOf(
|
|
hostRewardRecipient.address
|
|
)
|
|
|
|
expect(endBalanceHost).to.equal(startBalanceHost)
|
|
expect(endBalanceReward - startBalanceReward).to.gt(0)
|
|
})
|
|
|
|
it("pays the host when contract was cancelled", async function () {
|
|
// Lets advance the time more into the expiry window
|
|
const filledAt = (await currentTime()) + Math.floor(request.expiry / 3)
|
|
const expiresAt = (
|
|
await marketplace.requestExpiry(requestId(request))
|
|
).toNumber()
|
|
|
|
await marketplace.reserveSlot(slot.request, slot.index)
|
|
await setNextBlockTimestamp(filledAt)
|
|
await marketplace.fillSlot(slot.request, slot.index, proof)
|
|
await waitUntilCancelled(marketplace, request)
|
|
await marketplace.freeSlot(slotId(slot))
|
|
|
|
const expectedPartialPayout =
|
|
(expiresAt - filledAt) * pricePerSlotPerSecond(request)
|
|
const endBalance = await token.balanceOf(host.address)
|
|
expect(endBalance - ACCOUNT_STARTING_BALANCE).to.be.equal(
|
|
expectedPartialPayout
|
|
)
|
|
})
|
|
|
|
it("pays to host reward address when contract was cancelled, and returns collateral to host address", async function () {
|
|
// Lets advance the time more into the expiry window
|
|
const filledAt = (await currentTime()) + Math.floor(request.expiry / 3)
|
|
const expiresAt = (
|
|
await marketplace.requestExpiry(requestId(request))
|
|
).toNumber()
|
|
|
|
await marketplace.reserveSlot(slot.request, slot.index)
|
|
await setNextBlockTimestamp(filledAt)
|
|
await marketplace.fillSlot(slot.request, slot.index, proof)
|
|
await waitUntilCancelled(marketplace, request)
|
|
const startBalanceHost = await token.balanceOf(host.address)
|
|
const startBalanceReward = await token.balanceOf(
|
|
hostRewardRecipient.address
|
|
)
|
|
const startBalanceCollateral = await token.balanceOf(
|
|
hostCollateralRecipient.address
|
|
)
|
|
|
|
const collateralToBeReturned = await marketplace.currentCollateral(
|
|
slotId(slot)
|
|
)
|
|
|
|
await marketplace.freeSlot(
|
|
slotId(slot),
|
|
hostRewardRecipient.address,
|
|
hostCollateralRecipient.address
|
|
)
|
|
|
|
const expectedPartialPayout =
|
|
(expiresAt - filledAt) * pricePerSlotPerSecond(request)
|
|
|
|
const endBalanceReward = await token.balanceOf(
|
|
hostRewardRecipient.address
|
|
)
|
|
expect(endBalanceReward - startBalanceReward).to.be.equal(
|
|
expectedPartialPayout
|
|
)
|
|
|
|
const endBalanceHost = await token.balanceOf(host.address)
|
|
expect(endBalanceHost).to.be.equal(startBalanceHost)
|
|
|
|
const endBalanceCollateral = await token.balanceOf(
|
|
hostCollateralRecipient.address
|
|
)
|
|
expect(endBalanceCollateral - startBalanceCollateral).to.be.equal(
|
|
collateralPerSlot(request)
|
|
)
|
|
|
|
expect(collateralToBeReturned).to.be.equal(collateralPerSlot(request))
|
|
})
|
|
|
|
it("does not pay when the contract hasn't ended", async function () {
|
|
await marketplace.reserveSlot(slot.request, slot.index)
|
|
await marketplace.fillSlot(slot.request, slot.index, proof)
|
|
const startBalanceHost = await token.balanceOf(host.address)
|
|
const startBalanceReward = await token.balanceOf(
|
|
hostRewardRecipient.address
|
|
)
|
|
const startBalanceCollateral = await token.balanceOf(
|
|
hostCollateralRecipient.address
|
|
)
|
|
await marketplace.freeSlot(slotId(slot))
|
|
const endBalanceHost = await token.balanceOf(host.address)
|
|
const endBalanceReward = await token.balanceOf(
|
|
hostRewardRecipient.address
|
|
)
|
|
const endBalanceCollateral = await token.balanceOf(
|
|
hostCollateralRecipient.address
|
|
)
|
|
expect(endBalanceHost).to.equal(startBalanceHost)
|
|
expect(endBalanceReward).to.equal(startBalanceReward)
|
|
expect(endBalanceCollateral).to.equal(startBalanceCollateral)
|
|
})
|
|
|
|
it("can only be done once", async function () {
|
|
await waitUntilStarted(marketplace, request, proof, token)
|
|
await waitUntilFinished(marketplace, requestId(request))
|
|
await marketplace.freeSlot(slotId(slot))
|
|
await expect(marketplace.freeSlot(slotId(slot))).to.be.revertedWith(
|
|
"Marketplace_AlreadyPaid"
|
|
)
|
|
})
|
|
|
|
it("cannot be filled again", async function () {
|
|
await waitUntilStarted(marketplace, request, proof, token)
|
|
await waitUntilFinished(marketplace, requestId(request))
|
|
await marketplace.freeSlot(slotId(slot))
|
|
await expect(marketplace.fillSlot(slot.request, slot.index, proof)).to.be
|
|
.reverted
|
|
})
|
|
})
|
|
|
|
describe("fulfilling a request", function () {
|
|
beforeEach(async function () {
|
|
switchAccount(client)
|
|
await token.approve(marketplace.address, maxPrice(request))
|
|
await marketplace.requestStorage(request)
|
|
switchAccount(host)
|
|
const collateral = collateralPerSlot(request)
|
|
await token.approve(marketplace.address, collateral)
|
|
})
|
|
|
|
it("emits event when all slots are filled", async function () {
|
|
const lastSlot = request.ask.slots - 1
|
|
await token.approve(
|
|
marketplace.address,
|
|
collateralPerSlot(request) * lastSlot
|
|
)
|
|
for (let i = 0; i < lastSlot; i++) {
|
|
await marketplace.reserveSlot(slot.request, i)
|
|
await marketplace.fillSlot(slot.request, i, proof)
|
|
}
|
|
|
|
const collateral = collateralPerSlot(request)
|
|
await token.approve(marketplace.address, collateral)
|
|
await marketplace.reserveSlot(slot.request, lastSlot)
|
|
await expect(marketplace.fillSlot(slot.request, lastSlot, proof))
|
|
.to.emit(marketplace, "RequestFulfilled")
|
|
.withArgs(requestId(request))
|
|
})
|
|
it("sets state when all slots are filled", async function () {
|
|
const slots = request.ask.slots
|
|
const collateral = collateralPerSlot(request)
|
|
await token.approve(marketplace.address, collateral * slots)
|
|
for (let i = 0; i < slots; i++) {
|
|
await marketplace.reserveSlot(slot.request, i)
|
|
await marketplace.fillSlot(slot.request, i, proof)
|
|
}
|
|
await expect(await marketplace.requestState(slot.request)).to.equal(
|
|
RequestState.Started
|
|
)
|
|
})
|
|
it("fails when all slots are already filled", async function () {
|
|
const lastSlot = request.ask.slots - 1
|
|
await token.approve(
|
|
marketplace.address,
|
|
collateralPerSlot(request) * (lastSlot + 1)
|
|
)
|
|
for (let i = 0; i <= lastSlot; i++) {
|
|
await marketplace.reserveSlot(slot.request, i)
|
|
await marketplace.fillSlot(slot.request, i, proof)
|
|
}
|
|
await expect(
|
|
marketplace.fillSlot(slot.request, lastSlot, proof)
|
|
).to.be.revertedWith("Marketplace_SlotNotFree")
|
|
})
|
|
})
|
|
|
|
describe("withdrawing funds", function () {
|
|
beforeEach(async function () {
|
|
switchAccount(client)
|
|
await token.approve(marketplace.address, maxPrice(request))
|
|
await marketplace.requestStorage(request)
|
|
|
|
// wait a bit, so that there are funds for the client to withdraw
|
|
await advanceTime(10)
|
|
|
|
switchAccount(host)
|
|
const collateral = collateralPerSlot(request)
|
|
await token.approve(marketplace.address, collateral)
|
|
})
|
|
|
|
it("rejects withdraw when request not yet timed out", async function () {
|
|
switchAccount(client)
|
|
await expect(
|
|
marketplace.withdrawFunds(slot.request, clientWithdrawRecipient.address)
|
|
).to.be.revertedWith("Marketplace_InvalidState")
|
|
})
|
|
|
|
it("rejects withdraw when wrong account used", async function () {
|
|
await waitUntilCancelled(marketplace, request)
|
|
await expect(
|
|
marketplace.withdrawFunds(slot.request, clientWithdrawRecipient.address)
|
|
).to.be.revertedWith("Marketplace_InvalidClientAddress")
|
|
})
|
|
|
|
it("rejects withdraw when in wrong state", async function () {
|
|
// fill all slots, should change state to RequestState.Started
|
|
const lastSlot = request.ask.slots - 1
|
|
await token.approve(
|
|
marketplace.address,
|
|
collateralPerSlot(request) * (lastSlot + 1)
|
|
)
|
|
for (let i = 0; i <= lastSlot; i++) {
|
|
await marketplace.reserveSlot(slot.request, i)
|
|
await marketplace.fillSlot(slot.request, i, proof)
|
|
}
|
|
await waitUntilCancelled(marketplace, request)
|
|
switchAccount(client)
|
|
await expect(
|
|
marketplace.withdrawFunds(slot.request, clientWithdrawRecipient.address)
|
|
).to.be.revertedWith("Marketplace_InvalidState")
|
|
})
|
|
|
|
it("rejects withdraw when already withdrawn", async function () {
|
|
await waitUntilStarted(marketplace, request, proof, token)
|
|
await waitUntilFinished(marketplace, requestId(request))
|
|
|
|
switchAccount(client)
|
|
await marketplace.withdrawFunds(
|
|
slot.request,
|
|
clientWithdrawRecipient.address
|
|
)
|
|
await expect(
|
|
marketplace.withdrawFunds(slot.request, clientWithdrawRecipient.address)
|
|
).to.be.revertedWith("Marketplace_NothingToWithdraw")
|
|
})
|
|
|
|
it("emits event once request is cancelled", async function () {
|
|
await waitUntilCancelled(marketplace, request)
|
|
switchAccount(client)
|
|
await expect(
|
|
marketplace.withdrawFunds(slot.request, clientWithdrawRecipient.address)
|
|
)
|
|
.to.emit(marketplace, "RequestCancelled")
|
|
.withArgs(requestId(request))
|
|
})
|
|
|
|
it("withdraw rest of funds to the client payout address for finished requests", async function () {
|
|
await waitUntilStarted(marketplace, request, proof, token)
|
|
await waitUntilFinished(marketplace, requestId(request))
|
|
|
|
switchAccount(client)
|
|
const startBalanceClient = await token.balanceOf(client.address)
|
|
const startBalancePayout = await token.balanceOf(
|
|
clientWithdrawRecipient.address
|
|
)
|
|
await marketplace.withdrawFunds(
|
|
slot.request,
|
|
clientWithdrawRecipient.address
|
|
)
|
|
|
|
const endBalanceClient = await token.balanceOf(client.address)
|
|
const endBalancePayout = await token.balanceOf(
|
|
clientWithdrawRecipient.address
|
|
)
|
|
|
|
expect(endBalanceClient).to.equal(startBalanceClient)
|
|
// As all the request's slots will get filled and request will start and successfully finishes,
|
|
// then the upper bound to how much the client gets returned is the cumulative reward for all the
|
|
// slots for expiry window. This limit is "inclusive" because it is possible that all slots are filled
|
|
// at the time of expiry and hence the user would get the full "expiry window" reward back.
|
|
expect(endBalancePayout - startBalancePayout).to.be.gt(0)
|
|
expect(endBalancePayout - startBalancePayout).to.be.lte(
|
|
request.expiry * pricePerSlotPerSecond(request)
|
|
)
|
|
})
|
|
|
|
it("withdraws to the client payout address when request is cancelled", async function () {
|
|
await waitUntilCancelled(marketplace, request)
|
|
switchAccount(client)
|
|
const startBalanceClient = await token.balanceOf(client.address)
|
|
const startBalancePayout = await token.balanceOf(
|
|
clientWithdrawRecipient.address
|
|
)
|
|
await marketplace.withdrawFunds(
|
|
slot.request,
|
|
clientWithdrawRecipient.address
|
|
)
|
|
const endBalanceClient = await token.balanceOf(client.address)
|
|
const endBalancePayout = await token.balanceOf(
|
|
clientWithdrawRecipient.address
|
|
)
|
|
expect(endBalanceClient).to.equal(startBalanceClient)
|
|
expect(endBalancePayout - startBalancePayout).to.equal(maxPrice(request))
|
|
})
|
|
|
|
it("withdraws full price for failed requests to the client payout address", async function () {
|
|
await waitUntilStarted(marketplace, request, proof, token)
|
|
await waitUntilFailed(marketplace, request)
|
|
|
|
switchAccount(client)
|
|
|
|
const startBalanceClient = await token.balanceOf(client.address)
|
|
const startBalancePayout = await token.balanceOf(
|
|
clientWithdrawRecipient.address
|
|
)
|
|
await marketplace.withdrawFunds(
|
|
slot.request,
|
|
clientWithdrawRecipient.address
|
|
)
|
|
|
|
const endBalanceClient = await token.balanceOf(client.address)
|
|
const endBalancePayout = await token.balanceOf(
|
|
clientWithdrawRecipient.address
|
|
)
|
|
|
|
expect(endBalanceClient).to.equal(startBalanceClient)
|
|
expect(endBalancePayout - startBalancePayout).to.equal(maxPrice(request))
|
|
})
|
|
|
|
it("withdraws to the client payout address for cancelled requests lowered by hosts payout", async function () {
|
|
// Lets advance the time more into the expiry window
|
|
const filledAt = (await currentTime()) + Math.floor(request.expiry / 3)
|
|
const expiresAt = (
|
|
await marketplace.requestExpiry(requestId(request))
|
|
).toNumber()
|
|
|
|
await marketplace.reserveSlot(slot.request, slot.index)
|
|
await setNextBlockTimestamp(filledAt)
|
|
await marketplace.fillSlot(slot.request, slot.index, proof)
|
|
await waitUntilCancelled(marketplace, request)
|
|
const expectedPartialhostRewardRecipient =
|
|
(expiresAt - filledAt) * pricePerSlotPerSecond(request)
|
|
|
|
switchAccount(client)
|
|
await marketplace.withdrawFunds(
|
|
slot.request,
|
|
clientWithdrawRecipient.address
|
|
)
|
|
const endBalance = await token.balanceOf(clientWithdrawRecipient.address)
|
|
expect(endBalance - ACCOUNT_STARTING_BALANCE).to.equal(
|
|
maxPrice(request) - expectedPartialhostRewardRecipient
|
|
)
|
|
})
|
|
|
|
it("when slot is freed and not repaired, client will get refunded the freed slot's funds", async function () {
|
|
const payouts = await waitUntilStarted(marketplace, request, proof, token)
|
|
|
|
await expect(marketplace.freeSlot(slotId(slot))).to.emit(
|
|
marketplace,
|
|
"SlotFreed"
|
|
)
|
|
await waitUntilFinished(marketplace, requestId(request))
|
|
|
|
switchAccount(client)
|
|
await marketplace.withdrawFunds(
|
|
slot.request,
|
|
clientWithdrawRecipient.address
|
|
)
|
|
const endBalance = await token.balanceOf(clientWithdrawRecipient.address)
|
|
expect(endBalance - ACCOUNT_STARTING_BALANCE).to.equal(
|
|
maxPrice(request) -
|
|
payouts.reduce((a, b) => a + b, 0) + // This is the amount that user gets refunded for filling period in expiry window
|
|
payouts[slot.index] // This is the refunded amount for the freed slot
|
|
)
|
|
})
|
|
})
|
|
|
|
describe("request state", function () {
|
|
const { New, Cancelled, Started, Failed, Finished } = RequestState
|
|
|
|
beforeEach(async function () {
|
|
switchAccount(client)
|
|
await token.approve(marketplace.address, maxPrice(request))
|
|
await marketplace.requestStorage(request)
|
|
switchAccount(host)
|
|
const collateral = collateralPerSlot(request)
|
|
await token.approve(marketplace.address, collateral)
|
|
})
|
|
|
|
it("is 'New' initially", async function () {
|
|
expect(await marketplace.requestState(slot.request)).to.equal(New)
|
|
})
|
|
|
|
it("changes to 'Cancelled' once request is cancelled", async function () {
|
|
await waitUntilCancelled(marketplace, request)
|
|
expect(await marketplace.requestState(slot.request)).to.equal(Cancelled)
|
|
})
|
|
|
|
it("remains 'Cancelled' when client withdraws funds", async function () {
|
|
await waitUntilCancelled(marketplace, request)
|
|
switchAccount(client)
|
|
await marketplace.withdrawFunds(
|
|
slot.request,
|
|
clientWithdrawRecipient.address
|
|
)
|
|
expect(await marketplace.requestState(slot.request)).to.equal(Cancelled)
|
|
})
|
|
|
|
it("changes to 'Started' once all slots are filled", async function () {
|
|
await waitUntilStarted(marketplace, request, proof, token)
|
|
expect(await marketplace.requestState(slot.request)).to.equal(Started)
|
|
})
|
|
|
|
it("changes to 'Failed' once too many slots are freed", async function () {
|
|
await waitUntilStarted(marketplace, request, proof, token)
|
|
await waitUntilFailed(marketplace, request)
|
|
expect(await marketplace.requestState(slot.request)).to.equal(Failed)
|
|
})
|
|
|
|
it("does not change to 'Failed' before it is started", async function () {
|
|
await token.approve(
|
|
marketplace.address,
|
|
collateralPerSlot(request) * (request.ask.maxSlotLoss + 1)
|
|
)
|
|
for (let i = 0; i <= request.ask.maxSlotLoss; i++) {
|
|
await marketplace.reserveSlot(slot.request, i)
|
|
await marketplace.fillSlot(slot.request, i, proof)
|
|
}
|
|
for (let i = 0; i <= request.ask.maxSlotLoss; i++) {
|
|
slot.index = i
|
|
let id = slotId(slot)
|
|
await marketplace.forciblyFreeSlot(id)
|
|
}
|
|
expect(await marketplace.requestState(slot.request)).to.equal(New)
|
|
})
|
|
|
|
it("changes to 'Finished' when the request ends", async function () {
|
|
await waitUntilStarted(marketplace, request, proof, token)
|
|
await waitUntilFinished(marketplace, requestId(request))
|
|
expect(await marketplace.requestState(slot.request)).to.equal(Finished)
|
|
})
|
|
|
|
it("remains 'Finished' once a slot is paid out", async function () {
|
|
await waitUntilStarted(marketplace, request, proof, token)
|
|
await waitUntilFinished(marketplace, requestId(request))
|
|
await marketplace.freeSlot(slotId(slot))
|
|
expect(await marketplace.requestState(slot.request)).to.equal(Finished)
|
|
})
|
|
})
|
|
|
|
describe("slot state", function () {
|
|
const { Free, Filled, Finished, Failed, Paid, Cancelled, Repair } =
|
|
SlotState
|
|
let period, periodEnd
|
|
|
|
beforeEach(async function () {
|
|
period = config.proofs.period
|
|
;({ periodOf, periodEnd } = periodic(period))
|
|
|
|
switchAccount(client)
|
|
await token.approve(marketplace.address, maxPrice(request))
|
|
await marketplace.requestStorage(request)
|
|
switchAccount(host)
|
|
const collateral = collateralPerSlot(request)
|
|
await token.approve(marketplace.address, collateral)
|
|
})
|
|
|
|
async function waitUntilProofIsRequired(id) {
|
|
await advanceTimeTo(periodEnd(periodOf(await currentTime())))
|
|
while (
|
|
!(
|
|
(await marketplace.isProofRequired(id)) &&
|
|
(await marketplace.getPointer(id)) < 250
|
|
)
|
|
) {
|
|
await advanceTime(period)
|
|
}
|
|
}
|
|
|
|
it("is 'Free' initially", async function () {
|
|
expect(await marketplace.slotState(slotId(slot))).to.equal(Free)
|
|
})
|
|
|
|
it("changes to 'Filled' when slot is filled", async function () {
|
|
await marketplace.reserveSlot(slot.request, slot.index)
|
|
await marketplace.fillSlot(slot.request, slot.index, proof)
|
|
expect(await marketplace.slotState(slotId(slot))).to.equal(Filled)
|
|
})
|
|
|
|
it("changes to 'Finished' when request finishes", async function () {
|
|
await waitUntilStarted(marketplace, request, proof, token)
|
|
await waitUntilFinished(marketplace, slot.request)
|
|
expect(await marketplace.slotState(slotId(slot))).to.equal(Finished)
|
|
})
|
|
|
|
it("changes to 'Cancelled' when request is cancelled", async function () {
|
|
await marketplace.reserveSlot(slot.request, slot.index)
|
|
await marketplace.fillSlot(slot.request, slot.index, proof)
|
|
await waitUntilCancelled(marketplace, request)
|
|
expect(await marketplace.slotState(slotId(slot))).to.equal(Cancelled)
|
|
})
|
|
|
|
it("changes to 'Repair' when host frees the slot", async function () {
|
|
await marketplace.reserveSlot(slot.request, slot.index)
|
|
await marketplace.fillSlot(slot.request, slot.index, proof)
|
|
await marketplace.freeSlot(slotId(slot))
|
|
expect(await marketplace.slotState(slotId(slot))).to.equal(Repair)
|
|
})
|
|
|
|
it("changes to 'Repair' when too many proofs are missed", async function () {
|
|
await waitUntilStarted(marketplace, request, proof, token)
|
|
while ((await marketplace.slotState(slotId(slot))) === Filled) {
|
|
await waitUntilProofIsRequired(slotId(slot))
|
|
const missedPeriod = periodOf(await currentTime())
|
|
await advanceTime(period + 1)
|
|
await marketplace.markProofAsMissing(slotId(slot), missedPeriod)
|
|
}
|
|
expect(await marketplace.slotState(slotId(slot))).to.equal(Repair)
|
|
})
|
|
|
|
it("changes to 'Failed' when request fails", async function () {
|
|
await waitUntilStarted(marketplace, request, proof, token)
|
|
await waitUntilSlotFailed(marketplace, request, slot)
|
|
expect(await marketplace.slotState(slotId(slot))).to.equal(Failed)
|
|
})
|
|
|
|
it("changes to 'Paid' when host has been paid", async function () {
|
|
await waitUntilStarted(marketplace, request, proof, token)
|
|
await waitUntilFinished(marketplace, slot.request)
|
|
await marketplace.freeSlot(slotId(slot))
|
|
expect(await marketplace.slotState(slotId(slot))).to.equal(Paid)
|
|
})
|
|
})
|
|
|
|
describe("slot probability", function () {
|
|
beforeEach(async function () {
|
|
switchAccount(client)
|
|
await token.approve(marketplace.address, maxPrice(request))
|
|
await marketplace.requestStorage(request)
|
|
switchAccount(host)
|
|
const collateral = collateralPerSlot(request)
|
|
await token.approve(marketplace.address, collateral)
|
|
})
|
|
|
|
it("calculates correctly the slot probability", async function () {
|
|
await marketplace.reserveSlot(slot.request, slot.index)
|
|
await marketplace.fillSlot(slot.request, slot.index, proof)
|
|
|
|
// request.ask.proofProbability = 4
|
|
// config.proofs.downtime = 64
|
|
// 4 * (256 - 64) / 256
|
|
const expectedProbability = 3
|
|
expect(await marketplace.slotProbability(slotId(slot))).to.equal(
|
|
expectedProbability
|
|
)
|
|
})
|
|
})
|
|
|
|
describe("proof requirements", function () {
|
|
let period, periodOf, periodEnd
|
|
|
|
beforeEach(async function () {
|
|
period = config.proofs.period
|
|
;({ periodOf, periodEnd } = periodic(period))
|
|
|
|
switchAccount(client)
|
|
await token.approve(marketplace.address, maxPrice(request))
|
|
await marketplace.requestStorage(request)
|
|
switchAccount(host)
|
|
const collateral = collateralPerSlot(request)
|
|
await token.approve(marketplace.address, collateral)
|
|
})
|
|
|
|
async function waitUntilProofWillBeRequired(id) {
|
|
while (!(await marketplace.willProofBeRequired(id))) {
|
|
await advanceTime(period)
|
|
}
|
|
}
|
|
|
|
async function waitUntilProofIsRequired(id) {
|
|
await advanceTimeTo(periodEnd(periodOf(await currentTime())))
|
|
while (
|
|
!(
|
|
(await marketplace.isProofRequired(id)) &&
|
|
(await marketplace.getPointer(id)) < 250
|
|
)
|
|
) {
|
|
await advanceTime(period)
|
|
}
|
|
}
|
|
|
|
it("requires proofs when slot is filled", async function () {
|
|
const id = slotId(slot)
|
|
await marketplace.reserveSlot(slot.request, slot.index)
|
|
await marketplace.fillSlot(slot.request, slot.index, proof)
|
|
await waitUntilProofWillBeRequired(id)
|
|
})
|
|
|
|
it("will not require proofs once cancelled", async function () {
|
|
const id = slotId(slot)
|
|
await marketplace.reserveSlot(slot.request, slot.index)
|
|
await marketplace.fillSlot(slot.request, slot.index, proof)
|
|
await waitUntilProofWillBeRequired(id)
|
|
await expect(await marketplace.willProofBeRequired(id)).to.be.true
|
|
await waitUntilCancelled(marketplace, request)
|
|
await expect(await marketplace.willProofBeRequired(id)).to.be.false
|
|
})
|
|
|
|
it("does not require proofs once cancelled", async function () {
|
|
const id = slotId(slot)
|
|
await marketplace.reserveSlot(slot.request, slot.index)
|
|
await marketplace.fillSlot(slot.request, slot.index, proof)
|
|
await waitUntilProofIsRequired(id)
|
|
await expect(await marketplace.isProofRequired(id)).to.be.true
|
|
await waitUntilCancelled(marketplace, request)
|
|
await expect(await marketplace.isProofRequired(id)).to.be.false
|
|
})
|
|
|
|
it("does not provide challenges once cancelled", async function () {
|
|
const id = slotId(slot)
|
|
await marketplace.reserveSlot(slot.request, slot.index)
|
|
await marketplace.fillSlot(slot.request, slot.index, proof)
|
|
await waitUntilProofIsRequired(id)
|
|
const challenge1 = await marketplace.getChallenge(id)
|
|
expect(BigNumber.from(challenge1).gt(0))
|
|
await waitUntilCancelled(marketplace, request)
|
|
const challenge2 = await marketplace.getChallenge(id)
|
|
expect(BigNumber.from(challenge2).isZero())
|
|
})
|
|
|
|
it("does not provide pointer once cancelled", async function () {
|
|
const id = slotId(slot)
|
|
await marketplace.reserveSlot(slot.request, slot.index)
|
|
await marketplace.fillSlot(slot.request, slot.index, proof)
|
|
await waitUntilProofIsRequired(id)
|
|
const challenge1 = await marketplace.getChallenge(id)
|
|
expect(BigNumber.from(challenge1).gt(0))
|
|
await waitUntilCancelled(marketplace, request)
|
|
const challenge2 = await marketplace.getChallenge(id)
|
|
expect(BigNumber.from(challenge2).isZero())
|
|
})
|
|
})
|
|
|
|
describe("missing proofs", function () {
|
|
let period, periodOf, periodEnd
|
|
|
|
beforeEach(async function () {
|
|
period = config.proofs.period
|
|
;({ periodOf, periodEnd } = periodic(period))
|
|
|
|
switchAccount(client)
|
|
await token.approve(marketplace.address, maxPrice(request))
|
|
await marketplace.requestStorage(request)
|
|
switchAccount(host)
|
|
const collateral = collateralPerSlot(request)
|
|
await token.approve(marketplace.address, collateral)
|
|
})
|
|
|
|
async function waitUntilProofIsRequired(id) {
|
|
await advanceTimeTo(periodEnd(periodOf(await currentTime())))
|
|
while (
|
|
!(
|
|
(await marketplace.isProofRequired(id)) &&
|
|
(await marketplace.getPointer(id)) < 250
|
|
)
|
|
) {
|
|
await advanceTime(period)
|
|
}
|
|
}
|
|
|
|
it("fails to mark proof as missing when cancelled", async function () {
|
|
await marketplace.reserveSlot(slot.request, slot.index)
|
|
await marketplace.fillSlot(slot.request, slot.index, proof)
|
|
await waitUntilCancelled(marketplace, request)
|
|
let missedPeriod = periodOf(await currentTime())
|
|
await expect(
|
|
marketplace.markProofAsMissing(slotId(slot), missedPeriod)
|
|
).to.be.revertedWith("Marketplace_SlotNotAcceptingProofs")
|
|
})
|
|
|
|
describe("slashing when missing proofs", function () {
|
|
it("reduces collateral when a proof is missing", async function () {
|
|
const id = slotId(slot)
|
|
const { slashPercentage } = config.collateral
|
|
await marketplace.reserveSlot(slot.request, slot.index)
|
|
await marketplace.fillSlot(slot.request, slot.index, proof)
|
|
|
|
await waitUntilProofIsRequired(id)
|
|
let missedPeriod = periodOf(await currentTime())
|
|
await advanceTime(period + 1)
|
|
await marketplace.markProofAsMissing(id, missedPeriod)
|
|
|
|
const collateral = collateralPerSlot(request)
|
|
const expectedBalance = Math.round(
|
|
(collateral * (100 - slashPercentage)) / 100
|
|
)
|
|
|
|
expect(
|
|
BigNumber.from(expectedBalance).eq(
|
|
await marketplace.getSlotCollateral(id)
|
|
)
|
|
)
|
|
})
|
|
|
|
it("rewards validator when marking proof as missing", async function () {
|
|
const id = slotId(slot)
|
|
const { slashPercentage, validatorRewardPercentage } = config.collateral
|
|
await marketplace.reserveSlot(slot.request, slot.index)
|
|
await marketplace.fillSlot(slot.request, slot.index, proof)
|
|
|
|
switchAccount(validatorRecipient)
|
|
|
|
const startBalance = await token.balanceOf(validatorRecipient.address)
|
|
|
|
await waitUntilProofIsRequired(id)
|
|
let missedPeriod = periodOf(await currentTime())
|
|
await advanceTime(period + 1)
|
|
await marketplace.markProofAsMissing(id, missedPeriod)
|
|
|
|
const endBalance = await token.balanceOf(validatorRecipient.address)
|
|
|
|
const collateral = collateralPerSlot(request)
|
|
const slashedAmount = (collateral * slashPercentage) / 100
|
|
|
|
const expectedReward = Math.round(
|
|
(slashedAmount * validatorRewardPercentage) / 100
|
|
)
|
|
|
|
expect(endBalance.toNumber()).to.equal(
|
|
startBalance.toNumber() + expectedReward
|
|
)
|
|
})
|
|
})
|
|
|
|
it("frees slot when collateral slashed below minimum threshold", async function () {
|
|
const collateral = collateralPerSlot(request)
|
|
const minimum =
|
|
collateral -
|
|
(collateral *
|
|
config.collateral.maxNumberOfSlashes *
|
|
config.collateral.slashPercentage) /
|
|
100
|
|
await waitUntilStarted(marketplace, request, proof, token)
|
|
while ((await marketplace.slotState(slotId(slot))) === SlotState.Filled) {
|
|
expect(await marketplace.getSlotCollateral(slotId(slot))).to.be.gt(
|
|
minimum
|
|
)
|
|
await waitUntilProofIsRequired(slotId(slot))
|
|
const missedPeriod = periodOf(await currentTime())
|
|
await advanceTime(period + 1)
|
|
await marketplace.markProofAsMissing(slotId(slot), missedPeriod)
|
|
}
|
|
expect(await marketplace.slotState(slotId(slot))).to.equal(
|
|
SlotState.Repair
|
|
)
|
|
expect(await marketplace.getSlotCollateral(slotId(slot))).to.be.lte(
|
|
minimum
|
|
)
|
|
})
|
|
|
|
it("free slot when minimum reached and resets missed proof counter", async function () {
|
|
const collateral = collateralPerSlot(request)
|
|
const minimum =
|
|
collateral -
|
|
(collateral *
|
|
config.collateral.maxNumberOfSlashes *
|
|
config.collateral.slashPercentage) /
|
|
100
|
|
await waitUntilStarted(marketplace, request, proof, token)
|
|
let missedProofs = 0
|
|
while ((await marketplace.slotState(slotId(slot))) === SlotState.Filled) {
|
|
expect(await marketplace.getSlotCollateral(slotId(slot))).to.be.gt(
|
|
minimum
|
|
)
|
|
await waitUntilProofIsRequired(slotId(slot))
|
|
const missedPeriod = periodOf(await currentTime())
|
|
await advanceTime(period + 1)
|
|
expect(await marketplace.missingProofs(slotId(slot))).to.equal(
|
|
missedProofs
|
|
)
|
|
await marketplace.markProofAsMissing(slotId(slot), missedPeriod)
|
|
missedProofs += 1
|
|
}
|
|
expect(await marketplace.slotState(slotId(slot))).to.equal(
|
|
SlotState.Repair
|
|
)
|
|
expect(await marketplace.missingProofs(slotId(slot))).to.equal(0)
|
|
expect(await marketplace.getSlotCollateral(slotId(slot))).to.be.lte(
|
|
minimum
|
|
)
|
|
})
|
|
})
|
|
|
|
describe("list of active requests", function () {
|
|
beforeEach(async function () {
|
|
switchAccount(host)
|
|
const collateral = collateralPerSlot(request)
|
|
await token.approve(marketplace.address, collateral)
|
|
switchAccount(client)
|
|
await token.approve(marketplace.address, maxPrice(request))
|
|
})
|
|
|
|
it("adds request to list when requesting storage", async function () {
|
|
await marketplace.requestStorage(request)
|
|
expect(await marketplace.myRequests()).to.deep.equal([requestId(request)])
|
|
})
|
|
|
|
it("keeps request in list when cancelled", async function () {
|
|
await marketplace.requestStorage(request)
|
|
await waitUntilCancelled(marketplace, request)
|
|
expect(await marketplace.myRequests()).to.deep.equal([requestId(request)])
|
|
})
|
|
|
|
it("removes request from list when funds are withdrawn", async function () {
|
|
await marketplace.requestStorage(request)
|
|
await waitUntilCancelled(marketplace, request)
|
|
await marketplace.withdrawFunds(
|
|
requestId(request),
|
|
clientWithdrawRecipient.address
|
|
)
|
|
expect(await marketplace.myRequests()).to.deep.equal([])
|
|
})
|
|
|
|
it("keeps request in list when request fails", async function () {
|
|
await marketplace.requestStorage(request)
|
|
switchAccount(host)
|
|
await waitUntilStarted(marketplace, request, proof, token)
|
|
await waitUntilFailed(marketplace, request)
|
|
switchAccount(client)
|
|
expect(await marketplace.myRequests()).to.deep.equal([requestId(request)])
|
|
})
|
|
|
|
it("removes request from list when request finishes", async function () {
|
|
await marketplace.requestStorage(request)
|
|
switchAccount(host)
|
|
await waitUntilStarted(marketplace, request, proof, token)
|
|
await waitUntilFinished(marketplace, requestId(request))
|
|
await marketplace.freeSlot(slotId(slot))
|
|
switchAccount(client)
|
|
expect(await marketplace.myRequests()).to.deep.equal([])
|
|
})
|
|
})
|
|
|
|
describe("list of active slots", function () {
|
|
beforeEach(async function () {
|
|
switchAccount(client)
|
|
await token.approve(marketplace.address, maxPrice(request))
|
|
await marketplace.requestStorage(request)
|
|
switchAccount(host)
|
|
const collateral = collateralPerSlot(request)
|
|
await token.approve(marketplace.address, collateral)
|
|
})
|
|
|
|
it("adds slot to list when filling slot", async function () {
|
|
await marketplace.reserveSlot(slot.request, slot.index)
|
|
await marketplace.fillSlot(slot.request, slot.index, proof)
|
|
let slot1 = { ...slot, index: slot.index + 1 }
|
|
const collateral = collateralPerSlot(request)
|
|
await token.approve(marketplace.address, collateral)
|
|
await marketplace.reserveSlot(slot.request, slot1.index)
|
|
await marketplace.fillSlot(slot.request, slot1.index, proof)
|
|
expect(await marketplace.mySlots()).to.have.members([
|
|
slotId(slot),
|
|
slotId(slot1),
|
|
])
|
|
})
|
|
|
|
it("removes slot from list when slot is freed", async function () {
|
|
await marketplace.reserveSlot(slot.request, slot.index)
|
|
await marketplace.fillSlot(slot.request, slot.index, proof)
|
|
let slot1 = { ...slot, index: slot.index + 1 }
|
|
const collateral = collateralPerSlot(request)
|
|
await token.approve(marketplace.address, collateral)
|
|
await marketplace.reserveSlot(slot.request, slot1.index)
|
|
await marketplace.fillSlot(slot.request, slot1.index, proof)
|
|
await token.approve(marketplace.address, collateral)
|
|
await marketplace.freeSlot(slotId(slot))
|
|
expect(await marketplace.mySlots()).to.have.members([slotId(slot1)])
|
|
})
|
|
|
|
it("keeps slots when cancelled", async function () {
|
|
await marketplace.reserveSlot(slot.request, slot.index)
|
|
await marketplace.fillSlot(slot.request, slot.index, proof)
|
|
let slot1 = { ...slot, index: slot.index + 1 }
|
|
|
|
const collateral = collateralPerSlot(request)
|
|
await token.approve(marketplace.address, collateral)
|
|
await marketplace.reserveSlot(slot.request, slot1.index)
|
|
await marketplace.fillSlot(slot.request, slot1.index, proof)
|
|
await waitUntilCancelled(marketplace, request)
|
|
expect(await marketplace.mySlots()).to.have.members([
|
|
slotId(slot),
|
|
slotId(slot1),
|
|
])
|
|
})
|
|
|
|
it("removes slot when finished slot is freed", async function () {
|
|
await waitUntilStarted(marketplace, request, proof, token)
|
|
await waitUntilFinished(marketplace, requestId(request))
|
|
await marketplace.freeSlot(slotId(slot))
|
|
expect(await marketplace.mySlots()).to.not.contain(slotId(slot))
|
|
})
|
|
|
|
it("removes slot when cancelled slot is freed", async function () {
|
|
await marketplace.reserveSlot(slot.request, slot.index)
|
|
await marketplace.fillSlot(slot.request, slot.index, proof)
|
|
await waitUntilCancelled(marketplace, request)
|
|
await marketplace.freeSlot(slotId(slot))
|
|
expect(await marketplace.mySlots()).to.not.contain(slotId(slot))
|
|
})
|
|
|
|
it("removes slot when failed slot is freed", async function () {
|
|
await waitUntilStarted(marketplace, request, proof, token)
|
|
await waitUntilSlotFailed(marketplace, request, slot)
|
|
await marketplace.freeSlot(slotId(slot))
|
|
expect(await marketplace.mySlots()).to.not.contain(slotId(slot))
|
|
})
|
|
})
|
|
})
|