[marketplace] Add tests for freeing a slot
This commit is contained in:
parent
7487663534
commit
9f8affdcaa
|
@ -47,17 +47,19 @@ contract Collateral is AccountLocks {
|
||||||
assert(token.transfer(msg.sender, amount));
|
assert(token.transfer(msg.sender, amount));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function _slashAmount(address account, uint256 percentage)
|
||||||
|
internal
|
||||||
|
view
|
||||||
|
returns (uint256)
|
||||||
|
{
|
||||||
|
return (balanceOf(account) * percentage) / 100;
|
||||||
|
}
|
||||||
|
|
||||||
function _slash(address account, uint256 percentage)
|
function _slash(address account, uint256 percentage)
|
||||||
internal
|
internal
|
||||||
collateralInvariant
|
collateralInvariant
|
||||||
{
|
{
|
||||||
// TODO: perhaps we need to add a minCollateral parameter so that
|
uint256 amount = _slashAmount(account, percentage);
|
||||||
// a host's collateral can't drop below a certain amount, possibly
|
|
||||||
// preventing malicious behaviour when collateral drops too low for it
|
|
||||||
// to matter that it will be lost. Also, we need collateral to be high
|
|
||||||
// enough to cover repair costs in case of repair as well as marked
|
|
||||||
// proofs as missing fees.
|
|
||||||
uint256 amount = (balanceOf(account) * percentage) / 100;
|
|
||||||
funds.slashed += amount;
|
funds.slashed += amount;
|
||||||
subtract(account, amount);
|
subtract(account, amount);
|
||||||
}
|
}
|
||||||
|
|
|
@ -50,13 +50,11 @@ contract Marketplace is Collateral, Proofs {
|
||||||
function _freeSlot(
|
function _freeSlot(
|
||||||
bytes32 slotId
|
bytes32 slotId
|
||||||
) internal marketplaceInvariant {
|
) internal marketplaceInvariant {
|
||||||
bytes32 requestId = _getRequestIdForSlot(slotId);
|
Slot storage slot = _slot(slotId);
|
||||||
|
bytes32 requestId = slot.requestId;
|
||||||
RequestContext storage context = requestContexts[requestId];
|
RequestContext storage context = requestContexts[requestId];
|
||||||
require(context.state == RequestState.Started, "Invalid state");
|
require(context.state == RequestState.Started, "Invalid state");
|
||||||
require(!_isCancelled(requestId), "Request cancelled");
|
|
||||||
|
|
||||||
Slot storage slot = _slot(slotId);
|
|
||||||
require(slot.host != address(0), "Slot not filled");
|
|
||||||
|
|
||||||
_removeAccountLock(slot.host, requestId);
|
_removeAccountLock(slot.host, requestId);
|
||||||
// TODO: burn host's collateral except for repair costs + mark proof
|
// TODO: burn host's collateral except for repair costs + mark proof
|
||||||
|
|
|
@ -9,7 +9,7 @@ contract Storage is Collateral, Marketplace {
|
||||||
uint256 public collateralAmount;
|
uint256 public collateralAmount;
|
||||||
uint256 public slashMisses;
|
uint256 public slashMisses;
|
||||||
uint256 public slashPercentage;
|
uint256 public slashPercentage;
|
||||||
uint256 public missThreshold;
|
uint256 public minCollateralThreshold;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
IERC20 token,
|
IERC20 token,
|
||||||
|
@ -19,7 +19,7 @@ contract Storage is Collateral, Marketplace {
|
||||||
uint256 _collateralAmount,
|
uint256 _collateralAmount,
|
||||||
uint256 _slashMisses,
|
uint256 _slashMisses,
|
||||||
uint256 _slashPercentage,
|
uint256 _slashPercentage,
|
||||||
uint256 _missThreshold
|
uint256 _minCollateralThreshold
|
||||||
)
|
)
|
||||||
Marketplace(
|
Marketplace(
|
||||||
token,
|
token,
|
||||||
|
@ -32,7 +32,7 @@ contract Storage is Collateral, Marketplace {
|
||||||
collateralAmount = _collateralAmount;
|
collateralAmount = _collateralAmount;
|
||||||
slashMisses = _slashMisses;
|
slashMisses = _slashMisses;
|
||||||
slashPercentage = _slashPercentage;
|
slashPercentage = _slashPercentage;
|
||||||
missThreshold = _missThreshold;
|
minCollateralThreshold = _minCollateralThreshold;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getRequest(bytes32 requestId) public view returns (Request memory) {
|
function getRequest(bytes32 requestId) public view returns (Request memory) {
|
||||||
|
@ -43,8 +43,8 @@ contract Storage is Collateral, Marketplace {
|
||||||
return _slot(slotId);
|
return _slot(slotId);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getHost(bytes32 requestId) public view returns (address) {
|
function getHost(bytes32 slotId) public view returns (address) {
|
||||||
return _host(requestId);
|
return _host(slotId);
|
||||||
}
|
}
|
||||||
|
|
||||||
function missingProofs(bytes32 slotId) public view returns (uint256) {
|
function missingProofs(bytes32 slotId) public view returns (uint256) {
|
||||||
|
@ -85,12 +85,20 @@ contract Storage is Collateral, Marketplace {
|
||||||
slotMustAcceptProofs(slotId)
|
slotMustAcceptProofs(slotId)
|
||||||
{
|
{
|
||||||
_markProofAsMissing(slotId, period);
|
_markProofAsMissing(slotId, period);
|
||||||
uint256 missed = _missed(slotId);
|
address host = _host(slotId);
|
||||||
if (missed % slashMisses == 0) {
|
if (_missed(slotId) % slashMisses == 0) {
|
||||||
_slash(_host(slotId), slashPercentage);
|
|
||||||
}
|
uint256 slashAmount = _slashAmount(host, slashPercentage);
|
||||||
if (missed > missThreshold) {
|
if (balanceOf(host) - slashAmount < minCollateralThreshold) {
|
||||||
_freeSlot(slotId);
|
// If host has been slashed enough such that the next slashing would
|
||||||
|
// cause the collateral to drop below the minimum threshold, the slot
|
||||||
|
// needs to be freed as the remaining collateral must be used for
|
||||||
|
// repairs and rewards (with any leftover to be burnt).
|
||||||
|
_freeSlot(slotId);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
_slash(host, slashPercentage);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,4 +23,8 @@ contract TestCollateral is Collateral {
|
||||||
function unlock(bytes32 id) public {
|
function unlock(bytes32 id) public {
|
||||||
_unlock(id);
|
_unlock(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function removeAccountLock(address account, bytes32 lockId) public {
|
||||||
|
_removeAccountLock(account, lockId);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -25,4 +25,12 @@ contract TestMarketplace is Marketplace {
|
||||||
function isSlotCancelled(bytes32 slotId) public view returns (bool) {
|
function isSlotCancelled(bytes32 slotId) public view returns (bool) {
|
||||||
return _isSlotCancelled(slotId);
|
return _isSlotCancelled(slotId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function freeSlot(bytes32 slotId) public {
|
||||||
|
_freeSlot(slotId);
|
||||||
|
}
|
||||||
|
|
||||||
|
function slot(bytes32 slotId) public view returns (Slot memory) {
|
||||||
|
return _slot(slotId);
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -40,6 +40,10 @@ contract TestProofs is Proofs {
|
||||||
_expectProofs(id, _probability, _duration);
|
_expectProofs(id, _probability, _duration);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function unexpectProofs(bytes32 id) public {
|
||||||
|
_unexpectProofs(id);
|
||||||
|
}
|
||||||
|
|
||||||
function isProofRequired(bytes32 id) public view returns (bool) {
|
function isProofRequired(bytes32 id) public view returns (bool) {
|
||||||
return _isProofRequired(id);
|
return _isProofRequired(id);
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,36 @@
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
pragma solidity ^0.8.0;
|
||||||
|
|
||||||
|
import "./Storage.sol";
|
||||||
|
|
||||||
|
// exposes internal functions of Storage for testing
|
||||||
|
contract TestStorage is Storage {
|
||||||
|
constructor(
|
||||||
|
IERC20 token,
|
||||||
|
uint256 _proofPeriod,
|
||||||
|
uint256 _proofTimeout,
|
||||||
|
uint8 _proofDowntime,
|
||||||
|
uint256 _collateralAmount,
|
||||||
|
uint256 _slashMisses,
|
||||||
|
uint256 _slashPercentage,
|
||||||
|
uint256 _minCollateralThreshold
|
||||||
|
)
|
||||||
|
Storage(
|
||||||
|
token,
|
||||||
|
_proofPeriod,
|
||||||
|
_proofTimeout,
|
||||||
|
_proofDowntime,
|
||||||
|
_collateralAmount,
|
||||||
|
_slashMisses,
|
||||||
|
_slashPercentage,
|
||||||
|
_minCollateralThreshold
|
||||||
|
)
|
||||||
|
// solhint-disable-next-line no-empty-blocks
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
function slashAmount(address account, uint256 percentage) public view returns (uint256) {
|
||||||
|
return _slashAmount(account, percentage);
|
||||||
|
}
|
||||||
|
}
|
|
@ -6,7 +6,7 @@ async function deployStorage({ deployments, getNamedAccounts }) {
|
||||||
const collateralAmount = 100
|
const collateralAmount = 100
|
||||||
const slashMisses = 3
|
const slashMisses = 3
|
||||||
const slashPercentage = 10
|
const slashPercentage = 10
|
||||||
const missThreshold = 20
|
const minCollateralThreshold = 40
|
||||||
const args = [
|
const args = [
|
||||||
token.address,
|
token.address,
|
||||||
proofPeriod,
|
proofPeriod,
|
||||||
|
@ -15,10 +15,10 @@ async function deployStorage({ deployments, getNamedAccounts }) {
|
||||||
collateralAmount,
|
collateralAmount,
|
||||||
slashMisses,
|
slashMisses,
|
||||||
slashPercentage,
|
slashPercentage,
|
||||||
missThreshold,
|
minCollateralThreshold,
|
||||||
]
|
]
|
||||||
const { deployer } = await getNamedAccounts()
|
const { deployer } = await getNamedAccounts()
|
||||||
await deployments.deploy("Storage", { args, from: deployer })
|
await deployments.deploy("TestStorage", { args, from: deployer })
|
||||||
}
|
}
|
||||||
|
|
||||||
async function mine256blocks({ network, ethers }) {
|
async function mine256blocks({ network, ethers }) {
|
||||||
|
@ -32,5 +32,5 @@ module.exports = async (environment) => {
|
||||||
await deployStorage(environment)
|
await deployStorage(environment)
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports.tags = ["Storage"]
|
module.exports.tags = ["TestStorage"]
|
||||||
module.exports.dependencies = ["TestToken"]
|
module.exports.dependencies = ["TestToken"]
|
||||||
|
|
|
@ -110,5 +110,10 @@ describe("Collateral", function () {
|
||||||
await collateral.unlock(lock.id)
|
await collateral.unlock(lock.id)
|
||||||
await expect(collateral.withdraw()).not.to.be.reverted
|
await expect(collateral.withdraw()).not.to.be.reverted
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it("withdrawal succeeds when account lock has been removed", async function () {
|
||||||
|
await collateral.removeAccountLock(account0.address, lock.id)
|
||||||
|
await expect(collateral.withdraw()).not.to.be.reverted
|
||||||
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -103,6 +103,61 @@ describe("Marketplace", function () {
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe("freeing a slot", function () {
|
||||||
|
var id
|
||||||
|
beforeEach(async function () {
|
||||||
|
slot.index = 0
|
||||||
|
id = slotId(slot)
|
||||||
|
|
||||||
|
switchAccount(client)
|
||||||
|
await token.approve(marketplace.address, price(request))
|
||||||
|
await marketplace.requestStorage(request)
|
||||||
|
switchAccount(host)
|
||||||
|
await token.approve(marketplace.address, collateral)
|
||||||
|
await marketplace.deposit(collateral)
|
||||||
|
|
||||||
|
// await marketplace.fillSlot(slot.request, slot.index, proof)
|
||||||
|
})
|
||||||
|
|
||||||
|
async function waitUntilAllSlotsFilled() {
|
||||||
|
const lastSlot = request.ask.slots - 1
|
||||||
|
for (let i = 0; i <= lastSlot; i++) {
|
||||||
|
await marketplace.fillSlot(slot.request, i, proof)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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(
|
||||||
|
"Slot empty"
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("fails to free slot when not started", async function () {
|
||||||
|
await marketplace.fillSlot(slot.request, slot.index, proof)
|
||||||
|
await expect(marketplace.freeSlot(id)).to.be.revertedWith("Invalid state")
|
||||||
|
})
|
||||||
|
|
||||||
|
it("successfully frees slot", async function () {
|
||||||
|
await waitUntilAllSlotsFilled()
|
||||||
|
await expect(marketplace.freeSlot(id)).not.to.be.reverted
|
||||||
|
})
|
||||||
|
|
||||||
|
it("emits event once slot is freed", async function () {
|
||||||
|
await waitUntilAllSlotsFilled()
|
||||||
|
await expect(await marketplace.freeSlot(id))
|
||||||
|
.to.emit(marketplace, "SlotFreed")
|
||||||
|
.withArgs(slot.request, id)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("cannot get slot once freed", async function () {
|
||||||
|
await waitUntilAllSlotsFilled()
|
||||||
|
await marketplace.freeSlot(id)
|
||||||
|
await expect(marketplace.slot(id)).to.be.revertedWith("Slot empty")
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
describe("filling a slot", function () {
|
describe("filling a slot", function () {
|
||||||
beforeEach(async function () {
|
beforeEach(async function () {
|
||||||
switchAccount(client)
|
switchAccount(client)
|
||||||
|
|
|
@ -138,6 +138,13 @@ describe("Proofs", function () {
|
||||||
expect(await proofs.willProofBeRequired(id)).to.be.false
|
expect(await proofs.willProofBeRequired(id)).to.be.false
|
||||||
expect(await proofs.isProofRequired(id)).to.be.true
|
expect(await proofs.isProofRequired(id)).to.be.true
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it("proofs won't be required when no longer expected", async function () {
|
||||||
|
expect(await proofs.getPointer(id)).to.be.lt(downtime)
|
||||||
|
expect(await proofs.willProofBeRequired(id)).to.be.true
|
||||||
|
await proofs.unexpectProofs(id)
|
||||||
|
expect(await proofs.willProofBeRequired(id)).to.be.false
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe("when proofs are required", function () {
|
describe("when proofs are required", function () {
|
||||||
|
@ -256,5 +263,11 @@ describe("Proofs", function () {
|
||||||
proofs.markProofAsMissing(id, missedPeriod)
|
proofs.markProofAsMissing(id, missedPeriod)
|
||||||
).to.be.revertedWith("Proof already marked as missing")
|
).to.be.revertedWith("Proof already marked as missing")
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it("requires no proofs when no longer expected", async function () {
|
||||||
|
await waitUntilProofIsRequired(id)
|
||||||
|
await proofs.unexpectProofs(id)
|
||||||
|
await expect(await proofs.isProofRequired(id)).to.be.false
|
||||||
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -28,9 +28,9 @@ describe("Storage", function () {
|
||||||
beforeEach(async function () {
|
beforeEach(async function () {
|
||||||
;[client, host] = await ethers.getSigners()
|
;[client, host] = await ethers.getSigners()
|
||||||
|
|
||||||
await deployments.fixture(["TestToken", "Storage"])
|
await deployments.fixture(["TestToken", "TestStorage"])
|
||||||
token = await ethers.getContract("TestToken")
|
token = await ethers.getContract("TestToken")
|
||||||
storage = await ethers.getContract("Storage")
|
storage = await ethers.getContract("TestStorage")
|
||||||
|
|
||||||
await token.mint(client.address, 1_000_000_000)
|
await token.mint(client.address, 1_000_000_000)
|
||||||
await token.mint(host.address, 1_000_000_000)
|
await token.mint(host.address, 1_000_000_000)
|
||||||
|
@ -38,6 +38,7 @@ describe("Storage", function () {
|
||||||
collateralAmount = await storage.collateralAmount()
|
collateralAmount = await storage.collateralAmount()
|
||||||
slashMisses = await storage.slashMisses()
|
slashMisses = await storage.slashMisses()
|
||||||
slashPercentage = await storage.slashPercentage()
|
slashPercentage = await storage.slashPercentage()
|
||||||
|
minCollateralThreshold = await storage.minCollateralThreshold()
|
||||||
|
|
||||||
request = exampleRequest()
|
request = exampleRequest()
|
||||||
request.client = client.address
|
request.client = client.address
|
||||||
|
@ -190,6 +191,69 @@ describe("Storage", function () {
|
||||||
expect(BigNumber.from(challenge2).isZero())
|
expect(BigNumber.from(challenge2).isZero())
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe("freeing a slot", function () {
|
||||||
|
beforeEach(async function () {
|
||||||
|
period = (await storage.proofPeriod()).toNumber()
|
||||||
|
;({ periodOf, periodEnd } = periodic(period))
|
||||||
|
})
|
||||||
|
|
||||||
|
async function waitUntilAllSlotsFilled() {
|
||||||
|
const lastSlot = request.ask.slots - 1
|
||||||
|
for (let i = 0; i <= lastSlot; i++) {
|
||||||
|
await storage.fillSlot(slot.request, i, proof)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 waitUntilAllSlotsFilled()
|
||||||
|
|
||||||
|
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")
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
// TODO: implement checking of actual proofs of storage, instead of dummy bool
|
// TODO: implement checking of actual proofs of storage, instead of dummy bool
|
||||||
|
|
Loading…
Reference in New Issue