[marketplace] Add tests for freeing a slot

This commit is contained in:
Eric Mastro 2022-09-13 17:18:55 +10:00 committed by Eric Mastro
parent 7487663534
commit 9f8affdcaa
12 changed files with 225 additions and 28 deletions

View File

@ -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);
} }

View File

@ -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

View File

@ -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);
}
} }
} }
} }

View File

@ -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);
}
} }

View File

@ -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);
}
} }

View File

@ -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);
} }

36
contracts/TestStorage.sol Normal file
View File

@ -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);
}
}

View File

@ -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"]

View File

@ -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
})
}) })
}) })

View File

@ -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)

View File

@ -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
})
}) })
}) })

View File

@ -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