Skip to content

Commit dfab610

Browse files
authored
feat: repair reward (#193)
1 parent 06f9f56 commit dfab610

File tree

6 files changed

+86
-33
lines changed

6 files changed

+86
-33
lines changed

certora/specs/Marketplace.spec

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -301,7 +301,7 @@ rule functionsCausingSlotStateChanges(env e, method f) {
301301
assert slotStateBefore == Marketplace.SlotState.Finished && slotStateAfter == Marketplace.SlotState.Paid => canMakeSlotPaid(f);
302302

303303
// SlotState.Free -> SlotState.Filled
304-
assert slotStateBefore != Marketplace.SlotState.Filled && slotStateAfter == Marketplace.SlotState.Filled => canFillSlot(f);
304+
assert (slotStateBefore == Marketplace.SlotState.Free || slotStateBefore == Marketplace.SlotState.Repair) && slotStateAfter == Marketplace.SlotState.Filled => canFillSlot(f);
305305
}
306306

307307
rule allowedSlotStateChanges(env e, method f) {
@@ -326,8 +326,8 @@ rule allowedSlotStateChanges(env e, method f) {
326326
slotStateAfter == Marketplace.SlotState.Paid
327327
);
328328

329-
// SlotState.Filled only from Free
330-
assert slotStateBefore != Marketplace.SlotState.Filled && slotStateAfter == Marketplace.SlotState.Filled => slotStateBefore == Marketplace.SlotState.Free;
329+
// SlotState.Filled only from Free or Repair
330+
assert slotStateBefore != Marketplace.SlotState.Filled && slotStateAfter == Marketplace.SlotState.Filled => (slotStateBefore == Marketplace.SlotState.Free || slotStateBefore == Marketplace.SlotState.Repair);
331331
}
332332

333333
rule cancelledRequestsStayCancelled(env e, method f) {

contracts/Configuration.sol

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,7 @@ struct MarketplaceConfig {
1010
}
1111

1212
struct CollateralConfig {
13-
/// @dev percentage of remaining collateral slot after it has been freed
14-
/// (equivalent to `collateral - (collateral*maxNumberOfSlashes*slashPercentage)/100`)
15-
/// TODO: to be aligned more closely with actual cost of repair once bandwidth incentives are known,
16-
/// see https://github.com/codex-storage/codex-contracts-eth/pull/47#issuecomment-1465511949.
13+
/// @dev percentage of collateral that is used as repair reward
1714
uint8 repairRewardPercentage;
1815
uint8 maxNumberOfSlashes; // frees slot when the number of slashing reaches this value
1916
uint16 slashCriterion; // amount of proofs missed that lead to slashing

contracts/Marketplace.sol

Lines changed: 37 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,7 @@ contract Marketplace is SlotReservations, Proofs, StateRetrieval, Endian {
126126
}
127127

128128
/**
129-
* @notice Fills a slot. Reverts if an invalid proof of the slot data is
129+
* @notice Fills a slot. Reverts if an invalid proof of the slot data is
130130
provided.
131131
* @param requestId RequestId identifying the request containing the slot to
132132
fill.
@@ -149,36 +149,55 @@ contract Marketplace is SlotReservations, Proofs, StateRetrieval, Endian {
149149
slot.slotIndex = slotIndex;
150150
RequestContext storage context = _requestContexts[requestId];
151151

152-
require(slotState(slotId) == SlotState.Free, "Slot is not free");
152+
require(
153+
slotState(slotId) == SlotState.Free ||
154+
slotState(slotId) == SlotState.Repair,
155+
"Slot is not free"
156+
);
153157

154158
_startRequiringProofs(slotId, request.ask.proofProbability);
155159
submitProof(slotId, proof);
156160

157161
slot.host = msg.sender;
158-
slot.state = SlotState.Filled;
159162
slot.filledAt = block.timestamp;
160163

161164
context.slotsFilled += 1;
162165
context.fundsToReturnToClient -= _slotPayout(requestId, slot.filledAt);
163166

164167
// Collect collateral
165-
uint256 collateralAmount = request.ask.collateral;
168+
uint256 collateralAmount;
169+
if (slotState(slotId) == SlotState.Repair) {
170+
// Host is repairing a slot and is entitled for repair reward, so he gets "discounted collateral"
171+
// in this way he gets "physically" the reward at the end of the request when the full amount of collateral
172+
// is returned to him.
173+
collateralAmount =
174+
request.ask.collateral -
175+
((request.ask.collateral * _config.collateral.repairRewardPercentage) /
176+
100);
177+
} else {
178+
collateralAmount = request.ask.collateral;
179+
}
166180
_transferFrom(msg.sender, collateralAmount);
167181
_marketplaceTotals.received += collateralAmount;
168-
slot.currentCollateral = collateralAmount;
182+
slot.currentCollateral = request.ask.collateral; // Even if he has collateral discounted, he is operating with full collateral
169183

170184
_addToMySlots(slot.host, slotId);
171185

186+
slot.state = SlotState.Filled;
172187
emit SlotFilled(requestId, slotIndex);
173-
if (context.slotsFilled == request.ask.slots) {
188+
189+
if (
190+
context.slotsFilled == request.ask.slots &&
191+
context.state == RequestState.New // Only New requests can "start" the requests
192+
) {
174193
context.state = RequestState.Started;
175194
context.startedAt = block.timestamp;
176195
emit RequestFulfilled(requestId);
177196
}
178197
}
179198

180199
/**
181-
* @notice Frees a slot, paying out rewards and returning collateral for
200+
* @notice Frees a slot, paying out rewards and returning collateral for
182201
finished or cancelled requests to the host that has filled the slot.
183202
* @param slotId id of the slot to free
184203
* @dev The host that filled the slot must have initiated the transaction
@@ -190,7 +209,7 @@ contract Marketplace is SlotReservations, Proofs, StateRetrieval, Endian {
190209
}
191210

192211
/**
193-
* @notice Frees a slot, paying out rewards and returning collateral for
212+
* @notice Frees a slot, paying out rewards and returning collateral for
194213
finished or cancelled requests.
195214
* @param slotId id of the slot to free
196215
* @param rewardRecipient address to send rewards to
@@ -280,7 +299,7 @@ contract Marketplace is SlotReservations, Proofs, StateRetrieval, Endian {
280299
}
281300

282301
/**
283-
* @notice Abandons the slot without returning collateral, effectively slashing the
302+
* @notice Abandons the slot without returning collateral, effectively slashing the
284303
entire collateral.
285304
* @param slotId SlotId of the slot to free.
286305
* @dev _slots[slotId] is deleted, resetting _slots[slotId].currentCollateral
@@ -296,10 +315,13 @@ contract Marketplace is SlotReservations, Proofs, StateRetrieval, Endian {
296315
context.fundsToReturnToClient += _slotPayout(requestId, slot.filledAt);
297316

298317
_removeFromMySlots(slot.host, slotId);
299-
uint256 slotIndex = slot.slotIndex;
300-
delete _slots[slotId];
318+
delete _reservations[slotId]; // We purge all the reservations for the slot
319+
slot.state = SlotState.Repair;
320+
slot.filledAt = 0;
321+
slot.currentCollateral = 0;
322+
slot.host = address(0);
301323
context.slotsFilled -= 1;
302-
emit SlotFreed(requestId, slotIndex);
324+
emit SlotFreed(requestId, slot.slotIndex);
303325
_resetMissingProofs(slotId);
304326

305327
Request storage request = _requests[requestId];
@@ -337,7 +359,7 @@ contract Marketplace is SlotReservations, Proofs, StateRetrieval, Endian {
337359
}
338360

339361
/**
340-
* @notice Pays out a host for duration of time that the slot was filled, and
362+
* @notice Pays out a host for duration of time that the slot was filled, and
341363
returns the collateral.
342364
* @dev The payouts are sent to the rewardRecipient, and collateral is returned
343365
to the host address.
@@ -367,7 +389,7 @@ contract Marketplace is SlotReservations, Proofs, StateRetrieval, Endian {
367389
}
368390

369391
/**
370-
* @notice Withdraws remaining storage request funds back to the client that
392+
* @notice Withdraws remaining storage request funds back to the client that
371393
deposited them.
372394
* @dev Request must be cancelled, failed or finished, and the
373395
transaction must originate from the depositor address.
@@ -378,7 +400,7 @@ contract Marketplace is SlotReservations, Proofs, StateRetrieval, Endian {
378400
}
379401

380402
/**
381-
* @notice Withdraws storage request funds to the provided address.
403+
* @notice Withdraws storage request funds to the provided address.
382404
* @dev Request must be expired, must be in RequestState.New, and the
383405
transaction must originate from the depositer address.
384406
* @param requestId the id of the request

contracts/Requests.sol

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,12 +36,13 @@ enum RequestState {
3636
}
3737

3838
enum SlotState {
39-
Free, // [default] not filled yet, or host has vacated the slot
39+
Free, // [default] not filled yet
4040
Filled, // host has filled slot
4141
Finished, // successfully completed
4242
Failed, // the request has failed
4343
Paid, // host has been paid
44-
Cancelled // when request was cancelled then slot is cancelled as well
44+
Cancelled, // when request was cancelled then slot is cancelled as well
45+
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
4546
}
4647

4748
library Requests {

test/Marketplace.test.js

Lines changed: 41 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -257,6 +257,33 @@ describe("Marketplace", function () {
257257
expect(await marketplace.getHost(slotId(slot))).to.equal(host.address)
258258
})
259259

260+
it("gives discount on the collateral for repaired slot", async function () {
261+
await marketplace.reserveSlot(slot.request, slot.index)
262+
await marketplace.fillSlot(slot.request, slot.index, proof)
263+
await marketplace.freeSlot(slotId(slot))
264+
expect(await marketplace.slotState(slotId(slot))).to.equal(
265+
SlotState.Repair
266+
)
267+
268+
// We need to advance the time to next period, because filling slot
269+
// must not be done in the same period as for that period there was already proof
270+
// submitted with the previous `fillSlot` and the transaction would revert with "Proof already submitted".
271+
await advanceTimeForNextBlock(config.proofs.period + 1)
272+
273+
const startBalance = await token.balanceOf(host.address)
274+
const discountedCollateral =
275+
request.ask.collateral -
276+
(request.ask.collateral * config.collateral.repairRewardPercentage) /
277+
100
278+
await token.approve(marketplace.address, discountedCollateral)
279+
await marketplace.fillSlot(slot.request, slot.index, proof)
280+
const endBalance = await token.balanceOf(host.address)
281+
expect(startBalance - endBalance).to.equal(discountedCollateral)
282+
expect(await marketplace.slotState(slotId(slot))).to.equal(
283+
SlotState.Filled
284+
)
285+
})
286+
260287
it("fails to retrieve a request of an empty slot", async function () {
261288
expect(marketplace.getActiveSlot(slotId(slot))).to.be.revertedWith(
262289
"Slot is free"
@@ -371,11 +398,11 @@ describe("Marketplace", function () {
371398

372399
it("collects only requested collateral and not more", async function () {
373400
await token.approve(marketplace.address, request.ask.collateral * 2)
374-
const startBalanace = await token.balanceOf(host.address)
401+
const startBalance = await token.balanceOf(host.address)
375402
await marketplace.reserveSlot(slot.request, slot.index)
376403
await marketplace.fillSlot(slot.request, slot.index, proof)
377404
const endBalance = await token.balanceOf(host.address)
378-
expect(startBalanace - endBalance).to.eq(request.ask.collateral)
405+
expect(startBalance - endBalance).to.eq(request.ask.collateral)
379406
})
380407
})
381408

@@ -1015,7 +1042,8 @@ describe("Marketplace", function () {
10151042
})
10161043

10171044
describe("slot state", function () {
1018-
const { Free, Filled, Finished, Failed, Paid, Cancelled } = SlotState
1045+
const { Free, Filled, Finished, Failed, Paid, Cancelled, Repair } =
1046+
SlotState
10191047
let period, periodEnd
10201048

10211049
beforeEach(async function () {
@@ -1068,14 +1096,14 @@ describe("Marketplace", function () {
10681096
expect(await marketplace.slotState(slotId(slot))).to.equal(Cancelled)
10691097
})
10701098

1071-
it("changes to 'Free' when host frees the slot", async function () {
1099+
it("changes to 'Repair' when host frees the slot", async function () {
10721100
await marketplace.reserveSlot(slot.request, slot.index)
10731101
await marketplace.fillSlot(slot.request, slot.index, proof)
10741102
await marketplace.freeSlot(slotId(slot))
1075-
expect(await marketplace.slotState(slotId(slot))).to.equal(Free)
1103+
expect(await marketplace.slotState(slotId(slot))).to.equal(Repair)
10761104
})
10771105

1078-
it("changes to 'Free' when too many proofs are missed", async function () {
1106+
it("changes to 'Repair' when too many proofs are missed", async function () {
10791107
await waitUntilStarted(marketplace, request, proof, token)
10801108
while ((await marketplace.slotState(slotId(slot))) === Filled) {
10811109
await waitUntilProofIsRequired(slotId(slot))
@@ -1084,7 +1112,7 @@ describe("Marketplace", function () {
10841112
await mine()
10851113
await marketplace.markProofAsMissing(slotId(slot), missedPeriod)
10861114
}
1087-
expect(await marketplace.slotState(slotId(slot))).to.equal(Free)
1115+
expect(await marketplace.slotState(slotId(slot))).to.equal(Repair)
10881116
})
10891117

10901118
it("changes to 'Failed' when request fails", async function () {
@@ -1271,7 +1299,9 @@ describe("Marketplace", function () {
12711299
await advanceTimeForNextBlock(period + 1)
12721300
await marketplace.markProofAsMissing(slotId(slot), missedPeriod)
12731301
}
1274-
expect(await marketplace.slotState(slotId(slot))).to.equal(SlotState.Free)
1302+
expect(await marketplace.slotState(slotId(slot))).to.equal(
1303+
SlotState.Repair
1304+
)
12751305
expect(await marketplace.getSlotCollateral(slotId(slot))).to.be.lte(
12761306
minimum
12771307
)
@@ -1299,7 +1329,9 @@ describe("Marketplace", function () {
12991329
await marketplace.markProofAsMissing(slotId(slot), missedPeriod)
13001330
missedProofs += 1
13011331
}
1302-
expect(await marketplace.slotState(slotId(slot))).to.equal(SlotState.Free)
1332+
expect(await marketplace.slotState(slotId(slot))).to.equal(
1333+
SlotState.Repair
1334+
)
13031335
expect(await marketplace.missingProofs(slotId(slot))).to.equal(0)
13041336
expect(await marketplace.getSlotCollateral(slotId(slot))).to.be.lte(
13051337
minimum

test/requests.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ const SlotState = {
1616
Failed: 3,
1717
Paid: 4,
1818
Cancelled: 5,
19+
Repair: 6,
1920
}
2021

2122
function enableRequestAssertions() {

0 commit comments

Comments
 (0)