feat: repair reward (#193)

This commit is contained in:
Adam Uhlíř 2024-12-12 18:39:42 +01:00 committed by GitHub
parent 06f9f56cd2
commit dfab6102e7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 86 additions and 33 deletions

View File

@ -301,7 +301,7 @@ rule functionsCausingSlotStateChanges(env e, method f) {
assert slotStateBefore == Marketplace.SlotState.Finished && slotStateAfter == Marketplace.SlotState.Paid => canMakeSlotPaid(f); assert slotStateBefore == Marketplace.SlotState.Finished && slotStateAfter == Marketplace.SlotState.Paid => canMakeSlotPaid(f);
// SlotState.Free -> SlotState.Filled // 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) { rule allowedSlotStateChanges(env e, method f) {
@ -326,8 +326,8 @@ rule allowedSlotStateChanges(env e, method f) {
slotStateAfter == Marketplace.SlotState.Paid slotStateAfter == Marketplace.SlotState.Paid
); );
// SlotState.Filled only from Free // SlotState.Filled only from Free or Repair
assert slotStateBefore != Marketplace.SlotState.Filled && slotStateAfter == Marketplace.SlotState.Filled => slotStateBefore == Marketplace.SlotState.Free; assert slotStateBefore != Marketplace.SlotState.Filled && slotStateAfter == Marketplace.SlotState.Filled => (slotStateBefore == Marketplace.SlotState.Free || slotStateBefore == Marketplace.SlotState.Repair);
} }
rule cancelledRequestsStayCancelled(env e, method f) { rule cancelledRequestsStayCancelled(env e, method f) {

View File

@ -10,10 +10,7 @@ struct MarketplaceConfig {
} }
struct CollateralConfig { struct CollateralConfig {
/// @dev percentage of remaining collateral slot after it has been freed /// @dev percentage of collateral that is used as repair reward
/// (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.
uint8 repairRewardPercentage; uint8 repairRewardPercentage;
uint8 maxNumberOfSlashes; // frees slot when the number of slashing reaches this value uint8 maxNumberOfSlashes; // frees slot when the number of slashing reaches this value
uint16 slashCriterion; // amount of proofs missed that lead to slashing uint16 slashCriterion; // amount of proofs missed that lead to slashing

View File

@ -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. provided.
* @param requestId RequestId identifying the request containing the slot to * @param requestId RequestId identifying the request containing the slot to
fill. fill.
@ -149,28 +149,47 @@ contract Marketplace is SlotReservations, Proofs, StateRetrieval, Endian {
slot.slotIndex = slotIndex; slot.slotIndex = slotIndex;
RequestContext storage context = _requestContexts[requestId]; 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); _startRequiringProofs(slotId, request.ask.proofProbability);
submitProof(slotId, proof); submitProof(slotId, proof);
slot.host = msg.sender; slot.host = msg.sender;
slot.state = SlotState.Filled;
slot.filledAt = block.timestamp; slot.filledAt = block.timestamp;
context.slotsFilled += 1; context.slotsFilled += 1;
context.fundsToReturnToClient -= _slotPayout(requestId, slot.filledAt); context.fundsToReturnToClient -= _slotPayout(requestId, slot.filledAt);
// Collect collateral // 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); _transferFrom(msg.sender, collateralAmount);
_marketplaceTotals.received += 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); _addToMySlots(slot.host, slotId);
slot.state = SlotState.Filled;
emit SlotFilled(requestId, slotIndex); 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.state = RequestState.Started;
context.startedAt = block.timestamp; context.startedAt = block.timestamp;
emit RequestFulfilled(requestId); 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. finished or cancelled requests to the host that has filled the slot.
* @param slotId id of the slot to free * @param slotId id of the slot to free
* @dev The host that filled the slot must have initiated the transaction * @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. finished or cancelled requests.
* @param slotId id of the slot to free * @param slotId id of the slot to free
* @param rewardRecipient address to send rewards to * @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. entire collateral.
* @param slotId SlotId of the slot to free. * @param slotId SlotId of the slot to free.
* @dev _slots[slotId] is deleted, resetting _slots[slotId].currentCollateral * @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); context.fundsToReturnToClient += _slotPayout(requestId, slot.filledAt);
_removeFromMySlots(slot.host, slotId); _removeFromMySlots(slot.host, slotId);
uint256 slotIndex = slot.slotIndex; delete _reservations[slotId]; // We purge all the reservations for the slot
delete _slots[slotId]; slot.state = SlotState.Repair;
slot.filledAt = 0;
slot.currentCollateral = 0;
slot.host = address(0);
context.slotsFilled -= 1; context.slotsFilled -= 1;
emit SlotFreed(requestId, slotIndex); emit SlotFreed(requestId, slot.slotIndex);
_resetMissingProofs(slotId); _resetMissingProofs(slotId);
Request storage request = _requests[requestId]; 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. returns the collateral.
* @dev The payouts are sent to the rewardRecipient, and collateral is returned * @dev The payouts are sent to the rewardRecipient, and collateral is returned
to the host address. 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. deposited them.
* @dev Request must be cancelled, failed or finished, and the * @dev Request must be cancelled, failed or finished, and the
transaction must originate from the depositor address. 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 * @dev Request must be expired, must be in RequestState.New, and the
transaction must originate from the depositer address. transaction must originate from the depositer address.
* @param requestId the id of the request * @param requestId the id of the request

View File

@ -36,12 +36,13 @@ enum RequestState {
} }
enum SlotState { enum SlotState {
Free, // [default] not filled yet, or host has vacated the slot Free, // [default] not filled yet
Filled, // host has filled slot Filled, // host has filled slot
Finished, // successfully completed Finished, // successfully completed
Failed, // the request has failed Failed, // the request has failed
Paid, // host has been paid 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 { library Requests {

View File

@ -257,6 +257,33 @@ describe("Marketplace", function () {
expect(await marketplace.getHost(slotId(slot))).to.equal(host.address) 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 () { it("fails to retrieve a request of an empty slot", async function () {
expect(marketplace.getActiveSlot(slotId(slot))).to.be.revertedWith( expect(marketplace.getActiveSlot(slotId(slot))).to.be.revertedWith(
"Slot is free" "Slot is free"
@ -371,11 +398,11 @@ describe("Marketplace", function () {
it("collects only requested collateral and not more", async function () { it("collects only requested collateral and not more", async function () {
await token.approve(marketplace.address, request.ask.collateral * 2) 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.reserveSlot(slot.request, slot.index)
await marketplace.fillSlot(slot.request, slot.index, proof) await marketplace.fillSlot(slot.request, slot.index, proof)
const endBalance = await token.balanceOf(host.address) 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 () { describe("slot state", function () {
const { Free, Filled, Finished, Failed, Paid, Cancelled } = SlotState const { Free, Filled, Finished, Failed, Paid, Cancelled, Repair } =
SlotState
let period, periodEnd let period, periodEnd
beforeEach(async function () { beforeEach(async function () {
@ -1068,14 +1096,14 @@ describe("Marketplace", function () {
expect(await marketplace.slotState(slotId(slot))).to.equal(Cancelled) 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.reserveSlot(slot.request, slot.index)
await marketplace.fillSlot(slot.request, slot.index, proof) await marketplace.fillSlot(slot.request, slot.index, proof)
await marketplace.freeSlot(slotId(slot)) 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) await waitUntilStarted(marketplace, request, proof, token)
while ((await marketplace.slotState(slotId(slot))) === Filled) { while ((await marketplace.slotState(slotId(slot))) === Filled) {
await waitUntilProofIsRequired(slotId(slot)) await waitUntilProofIsRequired(slotId(slot))
@ -1084,7 +1112,7 @@ describe("Marketplace", function () {
await mine() await mine()
await marketplace.markProofAsMissing(slotId(slot), missedPeriod) 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 () { it("changes to 'Failed' when request fails", async function () {
@ -1271,7 +1299,9 @@ describe("Marketplace", function () {
await advanceTimeForNextBlock(period + 1) await advanceTimeForNextBlock(period + 1)
await marketplace.markProofAsMissing(slotId(slot), missedPeriod) 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( expect(await marketplace.getSlotCollateral(slotId(slot))).to.be.lte(
minimum minimum
) )
@ -1299,7 +1329,9 @@ describe("Marketplace", function () {
await marketplace.markProofAsMissing(slotId(slot), missedPeriod) await marketplace.markProofAsMissing(slotId(slot), missedPeriod)
missedProofs += 1 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.missingProofs(slotId(slot))).to.equal(0)
expect(await marketplace.getSlotCollateral(slotId(slot))).to.be.lte( expect(await marketplace.getSlotCollateral(slotId(slot))).to.be.lte(
minimum minimum

View File

@ -16,6 +16,7 @@ const SlotState = {
Failed: 3, Failed: 3,
Paid: 4, Paid: 4,
Cancelled: 5, Cancelled: 5,
Repair: 6,
} }
function enableRequestAssertions() { function enableRequestAssertions() {