diff --git a/contracts/Marketplace.sol b/contracts/Marketplace.sol index 13d08df..5e928d9 100644 --- a/contracts/Marketplace.sol +++ b/contracts/Marketplace.sol @@ -12,6 +12,10 @@ contract Marketplace is Collateral, Proofs { using EnumerableSet for EnumerableSet.Bytes32Set; uint256 public immutable collateral; + uint256 public immutable minCollateralThreshold; + uint256 public immutable slashMisses; + uint256 public immutable slashPercentage; + MarketplaceFunds private funds; mapping(RequestId => Request) private requests; mapping(RequestId => RequestContext) private requestContexts; @@ -22,6 +26,9 @@ contract Marketplace is Collateral, Proofs { constructor( IERC20 _token, uint256 _collateral, + uint256 _minCollateralThreshold, + uint256 _slashMisses, + uint256 _slashPercentage, uint256 _proofPeriod, uint256 _proofTimeout, uint8 _proofDowntime @@ -31,6 +38,9 @@ contract Marketplace is Collateral, Proofs { marketplaceInvariant { collateral = _collateral; + minCollateralThreshold = _minCollateralThreshold; + slashMisses = _slashMisses; + slashPercentage = _slashPercentage; } function myRequests() public view returns (RequestId[] memory) { @@ -119,6 +129,24 @@ contract Marketplace is Collateral, Proofs { } } + function markProofAsMissing(SlotId slotId, uint256 period) + public + slotMustAcceptProofs(slotId) + { + _markProofAsMissing(slotId, period); + address host = _host(slotId); + if (missingProofs(slotId) % slashMisses == 0) { + _slash(host, slashPercentage); + + if (balanceOf(host) < minCollateralThreshold) { + // When the collateral drops below the minimum threshold, the slot + // needs to be freed so that there is enough remaining collateral to be + // distributed for repairs and rewards (with any leftover to be burnt). + _forciblyFreeSlot(slotId); + } + } + } + function _forciblyFreeSlot(SlotId slotId) internal slotMustAcceptProofs(slotId) diff --git a/contracts/Storage.sol b/contracts/Storage.sol index 57259a0..5385a0e 100644 --- a/contracts/Storage.sol +++ b/contracts/Storage.sol @@ -6,11 +6,6 @@ import "./Proofs.sol"; import "./Collateral.sol"; contract Storage is Marketplace { - uint256 public collateralAmount; - uint256 public slashMisses; - uint256 public slashPercentage; - uint256 public minCollateralThreshold; - constructor( IERC20 token, uint256 _proofPeriod, @@ -24,32 +19,12 @@ contract Storage is Marketplace { Marketplace( token, _collateralAmount, + _minCollateralThreshold, + _slashMisses, + _slashPercentage, _proofPeriod, _proofTimeout, _proofDowntime ) - { - collateralAmount = _collateralAmount; - slashMisses = _slashMisses; - slashPercentage = _slashPercentage; - minCollateralThreshold = _minCollateralThreshold; - } - - function markProofAsMissing(SlotId slotId, uint256 period) - public - slotMustAcceptProofs(slotId) - { - _markProofAsMissing(slotId, period); - address host = _host(slotId); - if (missingProofs(slotId) % slashMisses == 0) { - _slash(host, slashPercentage); - - if (balanceOf(host) < minCollateralThreshold) { - // When the collateral drops below the minimum threshold, the slot - // needs to be freed so that there is enough remaining collateral to be - // distributed for repairs and rewards (with any leftover to be burnt). - _forciblyFreeSlot(slotId); - } - } - } + {} } diff --git a/contracts/TestMarketplace.sol b/contracts/TestMarketplace.sol index 1f67cf3..d53a5a1 100644 --- a/contracts/TestMarketplace.sol +++ b/contracts/TestMarketplace.sol @@ -8,6 +8,9 @@ contract TestMarketplace is Marketplace { constructor( IERC20 _token, uint256 _collateral, + uint256 _minCollateralThreshold, + uint256 _slashMisses, + uint256 _slashPercentage, uint256 _proofPeriod, uint256 _proofTimeout, uint8 _proofDowntime @@ -15,6 +18,9 @@ contract TestMarketplace is Marketplace { Marketplace( _token, _collateral, + _minCollateralThreshold, + _slashMisses, + _slashPercentage, _proofPeriod, _proofTimeout, _proofDowntime @@ -40,7 +46,9 @@ contract TestMarketplace is Marketplace { return _slot(slotId); } - function testAcceptsProofs(SlotId slotId) + function testAcceptsProofs( + SlotId slotId + ) public view slotMustAcceptProofs(slotId) diff --git a/test/Marketplace.test.js b/test/Marketplace.test.js index dd48569..c12d381 100644 --- a/test/Marketplace.test.js +++ b/test/Marketplace.test.js @@ -27,6 +27,9 @@ const { describe("Marketplace", function () { const collateral = 100 + const minCollateralThreshold = 40 + const slashMisses = 3 + const slashPercentage = 10 const proofPeriod = 10 const proofTimeout = 5 const proofDowntime = 64 @@ -54,6 +57,9 @@ describe("Marketplace", function () { marketplace = await Marketplace.deploy( token.address, collateral, + minCollateralThreshold, + slashMisses, + slashPercentage, proofPeriod, proofTimeout, proofDowntime @@ -752,6 +758,86 @@ describe("Marketplace", function () { }) }) + describe("missing proofs", function () { + let period, periodOf, periodEnd + + beforeEach(async function () { + period = (await marketplace.proofPeriod()).toNumber() + ;({ periodOf, periodEnd } = periodic(period)) + + 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) + }) + + 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.fillSlot(slot.request, slot.index, proof) + await waitUntilCancelled(request) + let missedPeriod = periodOf(await currentTime()) + await expect( + marketplace.markProofAsMissing(slotId(slot), missedPeriod) + ).to.be.revertedWith("Slot not accepting proofs") + }) + + describe("slashing when missing proofs", function () { + it("reduces collateral when too many proofs are missing", async function () { + const id = slotId(slot) + await marketplace.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 marketplace.markProofAsMissing(id, missedPeriod) + } + const expectedBalance = (collateral * (100 - slashPercentage)) / 100 + expect(await marketplace.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 waitUntilStarted(marketplace, 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 marketplace.markProofAsMissing(id, missedPeriod) + ).to.emit(marketplace, "SlotFreed") + expect(await marketplace.getHost(id)).to.equal(AddressZero) + } else { + await marketplace.markProofAsMissing(id, missedPeriod) + } + } + } + }) + }) + }) + describe("modifiers", function () { beforeEach(async function () { switchAccount(client) diff --git a/test/Storage.test.js b/test/Storage.test.js index 86a3780..d7193ab 100644 --- a/test/Storage.test.js +++ b/test/Storage.test.js @@ -38,7 +38,7 @@ describe("Storage", function () { await token.mint(client.address, 1_000_000_000) await token.mint(host.address, 1_000_000_000) - collateralAmount = await storage.collateralAmount() + collateralAmount = await storage.collateral() slashMisses = await storage.slashMisses() slashPercentage = await storage.slashPercentage() minCollateralThreshold = await storage.minCollateralThreshold() @@ -66,78 +66,6 @@ describe("Storage", function () { 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) - } - } - - it("fails to mark proof as missing when cancelled", async function () { - await storage.fillSlot(slot.request, slot.index, proof) - await waitUntilCancelled(request) - let missedPeriod = periodOf(await currentTime()) - await expect( - storage.markProofAsMissing(slotId(slot), missedPeriod) - ).to.be.revertedWith("Slot not accepting proofs") - }) - - 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 waitUntilStarted(storage, 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") - expect(await storage.getHost(id)).to.equal(AddressZero) - } else { - await storage.markProofAsMissing(id, missedPeriod) - } - } - } - }) - }) - }) }) // TODO: implement checking of actual proofs of storage, instead of dummy bool