diff --git a/certora/specs/Marketplace.spec b/certora/specs/Marketplace.spec index 59e31ee..2771657 100644 --- a/certora/specs/Marketplace.spec +++ b/certora/specs/Marketplace.spec @@ -301,7 +301,7 @@ rule functionsCausingSlotStateChanges(env e, method f) { assert slotStateBefore == Marketplace.SlotState.Finished && slotStateAfter == Marketplace.SlotState.Paid => canMakeSlotPaid(f); // SlotState.Free -> SlotState.Filled - assert slotStateBefore != Marketplace.SlotState.Filled && slotStateAfter == Marketplace.SlotState.Filled => canFillSlot(f); + assert (slotStateBefore == Marketplace.SlotState.Free || slotStateBefore == Marketplace.SlotState.Repair) && slotStateAfter == Marketplace.SlotState.Filled => canFillSlot(f); } rule allowedSlotStateChanges(env e, method f) { @@ -326,8 +326,8 @@ rule allowedSlotStateChanges(env e, method f) { slotStateAfter == Marketplace.SlotState.Paid ); - // SlotState.Filled only from Free - assert slotStateBefore != Marketplace.SlotState.Filled && slotStateAfter == Marketplace.SlotState.Filled => slotStateBefore == Marketplace.SlotState.Free; + // SlotState.Filled only from Free or Repair + assert slotStateBefore != Marketplace.SlotState.Filled && slotStateAfter == Marketplace.SlotState.Filled => (slotStateBefore == Marketplace.SlotState.Free || slotStateBefore == Marketplace.SlotState.Repair); } rule cancelledRequestsStayCancelled(env e, method f) { diff --git a/contracts/Configuration.sol b/contracts/Configuration.sol index 0f9e95d..5d7feed 100644 --- a/contracts/Configuration.sol +++ b/contracts/Configuration.sol @@ -10,10 +10,7 @@ struct MarketplaceConfig { } struct CollateralConfig { - /// @dev percentage of remaining collateral slot after it has been freed - /// (equivalent to `collateral - (collateral*maxNumberOfSlashes*slashPercentage)/100`) - /// TODO: to be aligned more closely with actual cost of repair once bandwidth incentives are known, - /// see https://github.com/codex-storage/codex-contracts-eth/pull/47#issuecomment-1465511949. + /// @dev percentage of collateral that is used as repair reward uint8 repairRewardPercentage; uint8 maxNumberOfSlashes; // frees slot when the number of slashing reaches this value uint16 slashCriterion; // amount of proofs missed that lead to slashing diff --git a/contracts/Marketplace.sol b/contracts/Marketplace.sol index 99b5837..e9597b2 100644 --- a/contracts/Marketplace.sol +++ b/contracts/Marketplace.sol @@ -126,7 +126,7 @@ contract Marketplace is SlotReservations, Proofs, StateRetrieval, Endian { } /** - * @notice Fills a slot. Reverts if an invalid proof of the slot data is + * @notice Fills a slot. Reverts if an invalid proof of the slot data is provided. * @param requestId RequestId identifying the request containing the slot to fill. @@ -149,28 +149,47 @@ contract Marketplace is SlotReservations, Proofs, StateRetrieval, Endian { slot.slotIndex = slotIndex; RequestContext storage context = _requestContexts[requestId]; - require(slotState(slotId) == SlotState.Free, "Slot is not free"); + require( + slotState(slotId) == SlotState.Free || + slotState(slotId) == SlotState.Repair, + "Slot is not free" + ); _startRequiringProofs(slotId, request.ask.proofProbability); submitProof(slotId, proof); slot.host = msg.sender; - slot.state = SlotState.Filled; slot.filledAt = block.timestamp; context.slotsFilled += 1; context.fundsToReturnToClient -= _slotPayout(requestId, slot.filledAt); // Collect collateral - uint256 collateralAmount = request.ask.collateral; + uint256 collateralAmount; + if (slotState(slotId) == SlotState.Repair) { + // Host is repairing a slot and is entitled for repair reward, so he gets "discounted collateral" + // in this way he gets "physically" the reward at the end of the request when the full amount of collateral + // is returned to him. + collateralAmount = + request.ask.collateral - + ((request.ask.collateral * _config.collateral.repairRewardPercentage) / + 100); + } else { + collateralAmount = request.ask.collateral; + } _transferFrom(msg.sender, collateralAmount); _marketplaceTotals.received += collateralAmount; - slot.currentCollateral = collateralAmount; + slot.currentCollateral = request.ask.collateral; // Even if he has collateral discounted, he is operating with full collateral _addToMySlots(slot.host, slotId); + slot.state = SlotState.Filled; emit SlotFilled(requestId, slotIndex); - if (context.slotsFilled == request.ask.slots) { + + if ( + context.slotsFilled == request.ask.slots && + context.state == RequestState.New // Only New requests can "start" the requests + ) { context.state = RequestState.Started; context.startedAt = block.timestamp; emit RequestFulfilled(requestId); @@ -178,7 +197,7 @@ contract Marketplace is SlotReservations, Proofs, StateRetrieval, Endian { } /** - * @notice Frees a slot, paying out rewards and returning collateral for + * @notice Frees a slot, paying out rewards and returning collateral for finished or cancelled requests to the host that has filled the slot. * @param slotId id of the slot to free * @dev The host that filled the slot must have initiated the transaction @@ -190,7 +209,7 @@ contract Marketplace is SlotReservations, Proofs, StateRetrieval, Endian { } /** - * @notice Frees a slot, paying out rewards and returning collateral for + * @notice Frees a slot, paying out rewards and returning collateral for finished or cancelled requests. * @param slotId id of the slot to free * @param rewardRecipient address to send rewards to @@ -280,7 +299,7 @@ contract Marketplace is SlotReservations, Proofs, StateRetrieval, Endian { } /** - * @notice Abandons the slot without returning collateral, effectively slashing the + * @notice Abandons the slot without returning collateral, effectively slashing the entire collateral. * @param slotId SlotId of the slot to free. * @dev _slots[slotId] is deleted, resetting _slots[slotId].currentCollateral @@ -296,10 +315,13 @@ contract Marketplace is SlotReservations, Proofs, StateRetrieval, Endian { context.fundsToReturnToClient += _slotPayout(requestId, slot.filledAt); _removeFromMySlots(slot.host, slotId); - uint256 slotIndex = slot.slotIndex; - delete _slots[slotId]; + delete _reservations[slotId]; // We purge all the reservations for the slot + slot.state = SlotState.Repair; + slot.filledAt = 0; + slot.currentCollateral = 0; + slot.host = address(0); context.slotsFilled -= 1; - emit SlotFreed(requestId, slotIndex); + emit SlotFreed(requestId, slot.slotIndex); _resetMissingProofs(slotId); Request storage request = _requests[requestId]; @@ -337,7 +359,7 @@ contract Marketplace is SlotReservations, Proofs, StateRetrieval, Endian { } /** - * @notice Pays out a host for duration of time that the slot was filled, and + * @notice Pays out a host for duration of time that the slot was filled, and returns the collateral. * @dev The payouts are sent to the rewardRecipient, and collateral is returned to the host address. @@ -367,7 +389,7 @@ contract Marketplace is SlotReservations, Proofs, StateRetrieval, Endian { } /** - * @notice Withdraws remaining storage request funds back to the client that + * @notice Withdraws remaining storage request funds back to the client that deposited them. * @dev Request must be cancelled, failed or finished, and the transaction must originate from the depositor address. @@ -378,7 +400,7 @@ contract Marketplace is SlotReservations, Proofs, StateRetrieval, Endian { } /** - * @notice Withdraws storage request funds to the provided address. + * @notice Withdraws storage request funds to the provided address. * @dev Request must be expired, must be in RequestState.New, and the transaction must originate from the depositer address. * @param requestId the id of the request diff --git a/contracts/Requests.sol b/contracts/Requests.sol index f4a159f..39ca1d3 100644 --- a/contracts/Requests.sol +++ b/contracts/Requests.sol @@ -36,12 +36,13 @@ enum RequestState { } enum SlotState { - Free, // [default] not filled yet, or host has vacated the slot + Free, // [default] not filled yet Filled, // host has filled slot Finished, // successfully completed Failed, // the request has failed Paid, // host has been paid - Cancelled // when request was cancelled then slot is cancelled as well + Cancelled, // when request was cancelled then slot is cancelled as well + Repair // when slot slot was forcible freed (host was kicked out from hosting the slot because of too many missed proofs) and needs to be repaired } library Requests { diff --git a/test/Marketplace.test.js b/test/Marketplace.test.js index 34c83d7..dcea214 100644 --- a/test/Marketplace.test.js +++ b/test/Marketplace.test.js @@ -257,6 +257,33 @@ describe("Marketplace", function () { 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 advanceTimeForNextBlock(config.proofs.period + 1) + + const startBalance = await token.balanceOf(host.address) + const discountedCollateral = + request.ask.collateral - + (request.ask.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( "Slot is free" @@ -371,11 +398,11 @@ describe("Marketplace", function () { it("collects only requested collateral and not more", async function () { await token.approve(marketplace.address, request.ask.collateral * 2) - const startBalanace = await token.balanceOf(host.address) + 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(startBalanace - endBalance).to.eq(request.ask.collateral) + expect(startBalance - endBalance).to.eq(request.ask.collateral) }) }) @@ -1015,7 +1042,8 @@ describe("Marketplace", function () { }) describe("slot state", function () { - const { Free, Filled, Finished, Failed, Paid, Cancelled } = SlotState + const { Free, Filled, Finished, Failed, Paid, Cancelled, Repair } = + SlotState let period, periodEnd beforeEach(async function () { @@ -1068,14 +1096,14 @@ describe("Marketplace", function () { expect(await marketplace.slotState(slotId(slot))).to.equal(Cancelled) }) - it("changes to 'Free' when host frees the slot", async function () { + 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(Free) + expect(await marketplace.slotState(slotId(slot))).to.equal(Repair) }) - it("changes to 'Free' when too many proofs are missed", async function () { + 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)) @@ -1084,7 +1112,7 @@ describe("Marketplace", function () { await mine() await marketplace.markProofAsMissing(slotId(slot), missedPeriod) } - expect(await marketplace.slotState(slotId(slot))).to.equal(Free) + expect(await marketplace.slotState(slotId(slot))).to.equal(Repair) }) it("changes to 'Failed' when request fails", async function () { @@ -1271,7 +1299,9 @@ describe("Marketplace", function () { await advanceTimeForNextBlock(period + 1) await marketplace.markProofAsMissing(slotId(slot), missedPeriod) } - expect(await marketplace.slotState(slotId(slot))).to.equal(SlotState.Free) + expect(await marketplace.slotState(slotId(slot))).to.equal( + SlotState.Repair + ) expect(await marketplace.getSlotCollateral(slotId(slot))).to.be.lte( minimum ) @@ -1299,7 +1329,9 @@ describe("Marketplace", function () { await marketplace.markProofAsMissing(slotId(slot), missedPeriod) missedProofs += 1 } - expect(await marketplace.slotState(slotId(slot))).to.equal(SlotState.Free) + 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 diff --git a/test/requests.js b/test/requests.js index e1f6233..53a18d7 100644 --- a/test/requests.js +++ b/test/requests.js @@ -16,6 +16,7 @@ const SlotState = { Failed: 3, Paid: 4, Cancelled: 5, + Repair: 6, } function enableRequestAssertions() {