mirror of
https://github.com/logos-storage/logos-storage-contracts-eth.git
synced 2026-01-04 06:13:09 +00:00
feat: repair reward (#193)
This commit is contained in:
parent
06f9f56cd2
commit
dfab6102e7
@ -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) {
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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 {
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -16,6 +16,7 @@ const SlotState = {
|
|||||||
Failed: 3,
|
Failed: 3,
|
||||||
Paid: 4,
|
Paid: 4,
|
||||||
Cancelled: 5,
|
Cancelled: 5,
|
||||||
|
Repair: 6,
|
||||||
}
|
}
|
||||||
|
|
||||||
function enableRequestAssertions() {
|
function enableRequestAssertions() {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user