mirror of
https://github.com/logos-storage/logos-storage-contracts-eth.git
synced 2026-01-06 07:13:07 +00:00
Allow proof ending to be extending once a contract is started, so that all filled slots share an ending time that is equal to the contract end time. Added tests. Add a mapping for proof id to endId so that proof expectations can be extended for all proofs that share a given endId. Add function modifiers that require the request state to allow proofs, with accompanying tests. General clean up of each function’s request state context, with accompanying tests. General clean up of all tests, including state change “wait” functions and normalising the time advancement functions.
317 lines
11 KiB
JavaScript
317 lines
11 KiB
JavaScript
const { expect } = require("chai")
|
|
const { ethers, deployments } = require("hardhat")
|
|
const { BigNumber } = ethers
|
|
const { hexlify, randomBytes } = ethers.utils
|
|
const { AddressZero } = ethers.constants
|
|
const { exampleRequest } = require("./examples")
|
|
const { advanceTime, advanceTimeTo, currentTime, mine } = require("./evm")
|
|
const { requestId, slotId } = require("./ids")
|
|
const { periodic } = require("./time")
|
|
const { price } = require("./price")
|
|
const {
|
|
waitUntilCancelled,
|
|
waitUntilStarted,
|
|
waitUntilFinished,
|
|
} = require("./marketplace")
|
|
|
|
describe("Storage", function () {
|
|
const proof = hexlify(randomBytes(42))
|
|
|
|
let storage
|
|
let token
|
|
let client, host
|
|
let request
|
|
let collateralAmount, slashMisses, slashPercentage
|
|
let slot
|
|
|
|
function switchAccount(account) {
|
|
token = token.connect(account)
|
|
storage = storage.connect(account)
|
|
}
|
|
|
|
beforeEach(async function () {
|
|
;[client, host] = await ethers.getSigners()
|
|
|
|
await deployments.fixture(["TestToken", "TestStorage"])
|
|
token = await ethers.getContract("TestToken")
|
|
storage = await ethers.getContract("TestStorage")
|
|
|
|
await token.mint(client.address, 1_000_000_000)
|
|
await token.mint(host.address, 1_000_000_000)
|
|
|
|
collateralAmount = await storage.collateralAmount()
|
|
slashMisses = await storage.slashMisses()
|
|
slashPercentage = await storage.slashPercentage()
|
|
minCollateralThreshold = await storage.minCollateralThreshold()
|
|
|
|
request = exampleRequest()
|
|
request.client = client.address
|
|
slot = {
|
|
request: requestId(request),
|
|
index: request.ask.slots / 2,
|
|
}
|
|
|
|
switchAccount(client)
|
|
await token.approve(storage.address, price(request))
|
|
await storage.requestStorage(request)
|
|
switchAccount(host)
|
|
await token.approve(storage.address, collateralAmount)
|
|
await storage.deposit(collateralAmount)
|
|
})
|
|
|
|
it("can retrieve storage requests", async function () {
|
|
const id = requestId(request)
|
|
const retrieved = await storage.getRequest(id)
|
|
expect(retrieved.client).to.equal(request.client)
|
|
expect(retrieved.expiry).to.equal(request.expiry)
|
|
expect(retrieved.nonce).to.equal(request.nonce)
|
|
})
|
|
|
|
it("can retrieve host that filled slot", async function () {
|
|
expect(await storage.getHost(slotId(slot))).to.equal(AddressZero)
|
|
await storage.fillSlot(slot.request, slot.index, proof)
|
|
expect(await storage.getHost(slotId(slot))).to.equal(host.address)
|
|
})
|
|
|
|
describe("ending the contract", function () {
|
|
it("unlocks the host collateral", async function () {
|
|
await storage.fillSlot(slot.request, slot.index, proof)
|
|
await waitUntilFinished(storage, slotId(slot))
|
|
await expect(storage.withdraw()).not.to.be.reverted
|
|
})
|
|
})
|
|
|
|
describe("missing proofs", function () {
|
|
let period, periodOf, periodEnd
|
|
|
|
beforeEach(async function () {
|
|
period = (await storage.proofPeriod()).toNumber()
|
|
;({ periodOf, periodEnd } = periodic(period))
|
|
})
|
|
|
|
async function waitUntilProofIsRequired(id) {
|
|
await advanceTimeTo(periodEnd(periodOf(await currentTime())))
|
|
while (
|
|
!(
|
|
(await storage.isProofRequired(id)) &&
|
|
(await storage.getPointer(id)) < 250
|
|
)
|
|
) {
|
|
await advanceTime(period)
|
|
}
|
|
}
|
|
|
|
describe("slashing when missing proofs", function () {
|
|
it("reduces collateral when too many proofs are missing", async function () {
|
|
const id = slotId(slot)
|
|
await storage.fillSlot(slot.request, slot.index, proof)
|
|
for (let i = 0; i < slashMisses; i++) {
|
|
await waitUntilProofIsRequired(id)
|
|
let missedPeriod = periodOf(await currentTime())
|
|
await advanceTime(period)
|
|
await storage.markProofAsMissing(id, missedPeriod)
|
|
}
|
|
const expectedBalance =
|
|
(collateralAmount * (100 - slashPercentage)) / 100
|
|
expect(await storage.balanceOf(host.address)).to.equal(expectedBalance)
|
|
})
|
|
})
|
|
|
|
describe("freeing a slot", function () {
|
|
it("frees slot when collateral slashed below minimum threshold", async function () {
|
|
const id = slotId(slot)
|
|
|
|
await waitUntilAllSlotsFilled(
|
|
storage,
|
|
request.ask.slots,
|
|
slot.request,
|
|
proof
|
|
)
|
|
|
|
// max slashes before dropping below collateral threshold
|
|
const maxSlashes = 10
|
|
for (let i = 0; i < maxSlashes; i++) {
|
|
for (let j = 0; j < slashMisses; j++) {
|
|
await waitUntilProofIsRequired(id)
|
|
let missedPeriod = periodOf(await currentTime())
|
|
await advanceTime(period)
|
|
if (i === maxSlashes - 1 && j === slashMisses - 1) {
|
|
await expect(
|
|
await storage.markProofAsMissing(id, missedPeriod)
|
|
).to.emit(storage, "SlotFreed")
|
|
await expect(storage.getSlot(id)).to.be.revertedWith("Slot empty")
|
|
} else {
|
|
await storage.markProofAsMissing(id, missedPeriod)
|
|
}
|
|
}
|
|
}
|
|
})
|
|
})
|
|
})
|
|
|
|
describe("contract state", function () {
|
|
let period, periodOf, periodEnd
|
|
|
|
beforeEach(async function () {
|
|
period = (await storage.proofPeriod()).toNumber()
|
|
;({ periodOf, periodEnd } = periodic(period))
|
|
})
|
|
|
|
async function waitUntilProofWillBeRequired(id) {
|
|
while (!(await storage.willProofBeRequired(id))) {
|
|
await mine()
|
|
}
|
|
}
|
|
|
|
async function waitUntilProofIsRequired(id) {
|
|
await advanceTimeTo(periodEnd(periodOf(await currentTime())))
|
|
while (
|
|
!(
|
|
(await storage.isProofRequired(id)) &&
|
|
(await storage.getPointer(id)) < 250
|
|
)
|
|
) {
|
|
await advanceTime(period)
|
|
}
|
|
}
|
|
|
|
it("fails to mark proof as missing when cancelled", async function () {
|
|
await storage.fillSlot(slot.request, slot.index, proof)
|
|
await waitUntilCancelled(request.expiry)
|
|
let missedPeriod = periodOf(await currentTime())
|
|
await expect(
|
|
storage.markProofAsMissing(slotId(slot), missedPeriod)
|
|
).to.be.revertedWith("Slot not accepting proofs")
|
|
})
|
|
|
|
it("will not require proofs once cancelled", async function () {
|
|
const id = slotId(slot)
|
|
await storage.fillSlot(slot.request, slot.index, proof)
|
|
await waitUntilProofWillBeRequired(id)
|
|
await expect(await storage.willProofBeRequired(id)).to.be.true
|
|
await advanceTimeTo(request.expiry + 1)
|
|
await expect(await storage.willProofBeRequired(id)).to.be.false
|
|
})
|
|
|
|
|
|
it("does not require proofs once cancelled", async function () {
|
|
const id = slotId(slot)
|
|
await storage.fillSlot(slot.request, slot.index, proof)
|
|
await waitUntilProofIsRequired(id)
|
|
await expect(await storage.isProofRequired(id)).to.be.true
|
|
await advanceTimeTo(request.expiry + 1)
|
|
await expect(await storage.isProofRequired(id)).to.be.false
|
|
})
|
|
|
|
it("does not provide challenges once cancelled", async function () {
|
|
const id = slotId(slot)
|
|
await storage.fillSlot(slot.request, slot.index, proof)
|
|
await waitUntilProofIsRequired(id)
|
|
const challenge1 = await storage.getChallenge(id)
|
|
expect(BigNumber.from(challenge1).gt(0))
|
|
await advanceTimeTo(request.expiry + 1)
|
|
const challenge2 = await storage.getChallenge(id)
|
|
expect(BigNumber.from(challenge2).isZero())
|
|
})
|
|
|
|
it("does not provide pointer once cancelled", async function () {
|
|
const id = slotId(slot)
|
|
await storage.fillSlot(slot.request, slot.index, proof)
|
|
await waitUntilProofIsRequired(id)
|
|
const challenge1 = await storage.getChallenge(id)
|
|
expect(BigNumber.from(challenge1).gt(0))
|
|
await advanceTimeTo(request.expiry + 1)
|
|
const challenge2 = await storage.getChallenge(id)
|
|
expect(BigNumber.from(challenge2).isZero())
|
|
})
|
|
})
|
|
describe("freeing a slot", function () {
|
|
beforeEach(async function () {
|
|
period = (await storage.proofPeriod()).toNumber()
|
|
;({ periodOf, periodEnd } = periodic(period))
|
|
})
|
|
|
|
async function waitUntilProofIsRequired(id) {
|
|
await advanceTimeTo(periodEnd(periodOf(await currentTime())))
|
|
while (
|
|
!(
|
|
(await storage.isProofRequired(id)) &&
|
|
(await storage.getPointer(id)) < 250
|
|
)
|
|
) {
|
|
await advanceTime(period)
|
|
}
|
|
}
|
|
|
|
async function markProofAsMissing(slotId, onMarkAsMissing) {
|
|
for (let i = 0; i < slashMisses; i++) {
|
|
await waitUntilProofIsRequired(slotId)
|
|
let missedPeriod = periodOf(await currentTime())
|
|
await advanceTime(period)
|
|
if (i === slashMisses - 1 && typeof onMarkAsMissing === "function") {
|
|
onMarkAsMissing(missedPeriod)
|
|
} else await storage.markProofAsMissing(slotId, missedPeriod)
|
|
}
|
|
}
|
|
|
|
it("frees slot when collateral slashed below minimum threshold", async function () {
|
|
const id = slotId(slot)
|
|
|
|
await waitUntilStarted(storage, request.ask.slots, slot.request, proof)
|
|
|
|
while (true) {
|
|
await markProofAsMissing(id)
|
|
let balance = await storage.balanceOf(host.address)
|
|
let slashAmount = await storage.slashAmount(
|
|
host.address,
|
|
slashPercentage
|
|
)
|
|
if (balance - slashAmount < minCollateralThreshold) {
|
|
break
|
|
}
|
|
}
|
|
|
|
let onMarkAsMissing = async function (missedPeriod) {
|
|
await expect(
|
|
await storage.markProofAsMissing(id, missedPeriod)
|
|
).to.emit(storage, "SlotFreed")
|
|
}
|
|
await markProofAsMissing(id, onMarkAsMissing)
|
|
await expect(storage.getSlot(id)).to.be.revertedWith("Slot empty")
|
|
})
|
|
})
|
|
describe("contract state", function () {
|
|
it("isCancelled is true once request is cancelled", async function () {
|
|
await expect(await storage.isCancelled(slot.request)).to.equal(false)
|
|
await waitUntilCancelled(request.expiry)
|
|
await expect(await storage.isCancelled(slot.request)).to.equal(true)
|
|
})
|
|
|
|
it("isSlotCancelled fails when slot is empty", async function () {
|
|
await expect(storage.isSlotCancelled(slotId(slot))).to.be.revertedWith(
|
|
"Slot empty"
|
|
)
|
|
})
|
|
|
|
it("isSlotCancelled is true once request is cancelled", async function () {
|
|
await storage.fillSlot(slot.request, slot.index, proof)
|
|
await waitUntilCancelled(request.expiry)
|
|
await expect(await storage.isSlotCancelled(slotId(slot))).to.equal(true)
|
|
})
|
|
|
|
it("isFinished is true once started and contract duration lapses", async function () {
|
|
await expect(await storage.isFinished(slot.request)).to.be.false
|
|
// fill all slots, should change state to RequestState.Started
|
|
await waitUntilStarted(storage, request.ask.slots, slot.request, proof)
|
|
await expect(await storage.isFinished(slot.request)).to.be.false
|
|
advanceTime(request.ask.duration + 1)
|
|
await expect(await storage.isFinished(slot.request)).to.be.true
|
|
})
|
|
})
|
|
})
|
|
|
|
// TODO: implement checking of actual proofs of storage, instead of dummy bool
|
|
// TODO: allow other host to take over contract when too many missed proofs
|
|
// TODO: small partial payouts when proofs are being submitted
|
|
// TODO: reward caller of markProofAsMissing
|