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);
// 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) {

View File

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

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

View File

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

View File

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

View File

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