mirror of
https://github.com/logos-storage/logos-storage-contracts-eth.git
synced 2026-01-05 23:03:12 +00:00
marketplace: burn tokens in vault when slashing
- move all collateral calculatons to separate library
This commit is contained in:
parent
639466662d
commit
bddbd02f02
@ -14,11 +14,9 @@ import "./StateRetrieval.sol";
|
|||||||
import "./Endian.sol";
|
import "./Endian.sol";
|
||||||
import "./Groth16.sol";
|
import "./Groth16.sol";
|
||||||
import "./marketplace/VaultHelpers.sol";
|
import "./marketplace/VaultHelpers.sol";
|
||||||
|
import "./marketplace/Collateral.sol";
|
||||||
|
|
||||||
contract Marketplace is SlotReservations, Proofs, StateRetrieval, Endian {
|
contract Marketplace is SlotReservations, Proofs, StateRetrieval, Endian {
|
||||||
error Marketplace_RepairRewardPercentageTooHigh();
|
|
||||||
error Marketplace_SlashPercentageTooHigh();
|
|
||||||
error Marketplace_MaximumSlashingTooHigh();
|
|
||||||
error Marketplace_InvalidExpiry();
|
error Marketplace_InvalidExpiry();
|
||||||
error Marketplace_InvalidMaxSlotLoss();
|
error Marketplace_InvalidMaxSlotLoss();
|
||||||
error Marketplace_InsufficientSlots();
|
error Marketplace_InsufficientSlots();
|
||||||
@ -49,6 +47,8 @@ contract Marketplace is SlotReservations, Proofs, StateRetrieval, Endian {
|
|||||||
using VaultHelpers for Vault;
|
using VaultHelpers for Vault;
|
||||||
using VaultHelpers for RequestId;
|
using VaultHelpers for RequestId;
|
||||||
using VaultHelpers for Request;
|
using VaultHelpers for Request;
|
||||||
|
using Collateral for Request;
|
||||||
|
using Collateral for CollateralConfig;
|
||||||
using Timestamps for Timestamp;
|
using Timestamps for Timestamp;
|
||||||
using Tokens for TokensPerSecond;
|
using Tokens for TokensPerSecond;
|
||||||
|
|
||||||
@ -75,14 +75,6 @@ contract Marketplace is SlotReservations, Proofs, StateRetrieval, Endian {
|
|||||||
/// based on time they actually host the content
|
/// based on time they actually host the content
|
||||||
Timestamp filledAt;
|
Timestamp filledAt;
|
||||||
uint64 slotIndex;
|
uint64 slotIndex;
|
||||||
/// @notice Tracks the current amount of host's collateral that is
|
|
||||||
/// to be payed out at the end of Slot's lifespan.
|
|
||||||
/// @dev When Slot is filled, the collateral is collected in amount
|
|
||||||
/// of request.ask.collateralPerByte * request.ask.slotSize
|
|
||||||
/// (== request.ask.collateralPerSlot() when using the AskHelpers library)
|
|
||||||
/// @dev When Host is slashed for missing a proof the slashed amount is
|
|
||||||
/// reflected in this variable
|
|
||||||
uint256 currentCollateral;
|
|
||||||
/// @notice address used for collateral interactions and identifying hosts
|
/// @notice address used for collateral interactions and identifying hosts
|
||||||
address host;
|
address host;
|
||||||
}
|
}
|
||||||
@ -98,18 +90,7 @@ contract Marketplace is SlotReservations, Proofs, StateRetrieval, Endian {
|
|||||||
IGroth16Verifier verifier
|
IGroth16Verifier verifier
|
||||||
) SlotReservations(config.reservations) Proofs(config.proofs, verifier) {
|
) SlotReservations(config.reservations) Proofs(config.proofs, verifier) {
|
||||||
_vault = vault_;
|
_vault = vault_;
|
||||||
|
config.collateral.checkCorrectness();
|
||||||
if (config.collateral.repairRewardPercentage > 100)
|
|
||||||
revert Marketplace_RepairRewardPercentageTooHigh();
|
|
||||||
if (config.collateral.slashPercentage > 100)
|
|
||||||
revert Marketplace_SlashPercentageTooHigh();
|
|
||||||
|
|
||||||
if (
|
|
||||||
config.collateral.maxNumberOfSlashes * config.collateral.slashPercentage >
|
|
||||||
100
|
|
||||||
) {
|
|
||||||
revert Marketplace_MaximumSlashingTooHigh();
|
|
||||||
}
|
|
||||||
_config = config;
|
_config = config;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -125,10 +106,6 @@ contract Marketplace is SlotReservations, Proofs, StateRetrieval, Endian {
|
|||||||
return _vault;
|
return _vault;
|
||||||
}
|
}
|
||||||
|
|
||||||
function currentCollateral(SlotId slotId) public view returns (uint256) {
|
|
||||||
return _slots[slotId].currentCollateral;
|
|
||||||
}
|
|
||||||
|
|
||||||
function requestStorage(Request calldata request) public {
|
function requestStorage(Request calldata request) public {
|
||||||
RequestId id = request.id();
|
RequestId id = request.id();
|
||||||
|
|
||||||
@ -226,17 +203,15 @@ contract Marketplace is SlotReservations, Proofs, StateRetrieval, Endian {
|
|||||||
context.slotsFilled += 1;
|
context.slotsFilled += 1;
|
||||||
|
|
||||||
// Collect collateral
|
// Collect collateral
|
||||||
uint128 collateralAmount;
|
uint128 collateralAmount = request.collateralPerSlot();
|
||||||
uint128 collateralPerSlot = request.ask.collateralPerSlot();
|
uint128 designatedAmount = _config.collateral.designatedCollateral(
|
||||||
|
collateralAmount
|
||||||
|
);
|
||||||
if (slotState(slotId) == SlotState.Repair) {
|
if (slotState(slotId) == SlotState.Repair) {
|
||||||
// Host is repairing a slot and is entitled for repair reward, so he gets "discounted collateral"
|
// 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
|
// in this way he gets "physically" the reward at the end of the request when the full amount of collateral
|
||||||
// is returned to him.
|
// is returned to him.
|
||||||
collateralAmount =
|
collateralAmount -= _config.collateral.repairReward(collateralAmount);
|
||||||
collateralPerSlot -
|
|
||||||
((collateralPerSlot * _config.collateral.repairRewardPercentage) / 100);
|
|
||||||
} else {
|
|
||||||
collateralAmount = collateralPerSlot;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
FundId fund = requestId.asFundId();
|
FundId fund = requestId.asFundId();
|
||||||
@ -245,10 +220,9 @@ contract Marketplace is SlotReservations, Proofs, StateRetrieval, Endian {
|
|||||||
TokensPerSecond rate = request.ask.pricePerSlotPerSecond();
|
TokensPerSecond rate = request.ask.pricePerSlotPerSecond();
|
||||||
|
|
||||||
_transferToVault(slot.host, fund, hostAccount, collateralAmount);
|
_transferToVault(slot.host, fund, hostAccount, collateralAmount);
|
||||||
|
_vault.designate(fund, hostAccount, designatedAmount);
|
||||||
_vault.flow(fund, clientAccount, hostAccount, rate);
|
_vault.flow(fund, clientAccount, hostAccount, rate);
|
||||||
|
|
||||||
slot.currentCollateral = collateralPerSlot; // Even if he has collateral discounted, he is operating with full collateral
|
|
||||||
|
|
||||||
_addToMySlots(slot.host, slotId);
|
_addToMySlots(slot.host, slotId);
|
||||||
|
|
||||||
slot.state = SlotState.Filled;
|
slot.state = SlotState.Filled;
|
||||||
@ -343,23 +317,16 @@ contract Marketplace is SlotReservations, Proofs, StateRetrieval, Endian {
|
|||||||
Slot storage slot = _slots[slotId];
|
Slot storage slot = _slots[slotId];
|
||||||
Request storage request = _requests[slot.requestId];
|
Request storage request = _requests[slot.requestId];
|
||||||
|
|
||||||
uint128 slashedAmount = (request.ask.collateralPerSlot() *
|
uint128 collateral = request.collateralPerSlot();
|
||||||
_config.collateral.slashPercentage) / 100;
|
uint128 slashedAmount = _config.collateral.slashAmount(collateral);
|
||||||
|
uint128 validatorReward = _config.collateral.validatorReward(slashedAmount);
|
||||||
uint128 validatorRewardAmount = (slashedAmount *
|
|
||||||
_config.collateral.validatorRewardPercentage) / 100;
|
|
||||||
|
|
||||||
FundId fund = slot.requestId.asFundId();
|
FundId fund = slot.requestId.asFundId();
|
||||||
AccountId hostAccount = _vault.hostAccount(slot.host, slot.slotIndex);
|
AccountId hostAccount = _vault.hostAccount(slot.host, slot.slotIndex);
|
||||||
AccountId validatorAccount = _vault.validatorAccount(msg.sender);
|
AccountId validatorAccount = _vault.validatorAccount(msg.sender);
|
||||||
_vault.transfer(
|
_vault.transfer(fund, hostAccount, validatorAccount, validatorReward);
|
||||||
fund,
|
_vault.burnDesignated(fund, hostAccount, slashedAmount - validatorReward);
|
||||||
hostAccount,
|
|
||||||
validatorAccount,
|
|
||||||
validatorRewardAmount
|
|
||||||
);
|
|
||||||
|
|
||||||
slot.currentCollateral -= slashedAmount;
|
|
||||||
if (missingProofs(slotId) >= _config.collateral.maxNumberOfSlashes) {
|
if (missingProofs(slotId) >= _config.collateral.maxNumberOfSlashes) {
|
||||||
// When the number of slashings is at or above the allowed amount,
|
// When the number of slashings is at or above the allowed amount,
|
||||||
// free the slot.
|
// free the slot.
|
||||||
@ -367,13 +334,7 @@ contract Marketplace is SlotReservations, Proofs, StateRetrieval, Endian {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/// Abandons the slot, burns all associated tokens
|
||||||
* @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
|
|
||||||
to 0.
|
|
||||||
*/
|
|
||||||
function _forciblyFreeSlot(SlotId slotId) internal {
|
function _forciblyFreeSlot(SlotId slotId) internal {
|
||||||
Slot storage slot = _slots[slotId];
|
Slot storage slot = _slots[slotId];
|
||||||
RequestId requestId = slot.requestId;
|
RequestId requestId = slot.requestId;
|
||||||
@ -393,7 +354,6 @@ contract Marketplace is SlotReservations, Proofs, StateRetrieval, Endian {
|
|||||||
delete _reservations[slotId]; // We purge all the reservations for the slot
|
delete _reservations[slotId]; // We purge all the reservations for the slot
|
||||||
slot.state = SlotState.Repair;
|
slot.state = SlotState.Repair;
|
||||||
slot.filledAt = Timestamp.wrap(0);
|
slot.filledAt = Timestamp.wrap(0);
|
||||||
slot.currentCollateral = 0;
|
|
||||||
slot.host = address(0);
|
slot.host = address(0);
|
||||||
context.slotsFilled -= 1;
|
context.slotsFilled -= 1;
|
||||||
emit SlotFreed(requestId, slot.slotIndex);
|
emit SlotFreed(requestId, slot.slotIndex);
|
||||||
|
|||||||
@ -49,10 +49,6 @@ enum SlotState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
library AskHelpers {
|
library AskHelpers {
|
||||||
function collateralPerSlot(Ask memory ask) internal pure returns (uint128) {
|
|
||||||
return ask.collateralPerByte * ask.slotSize;
|
|
||||||
}
|
|
||||||
|
|
||||||
function pricePerSlotPerSecond(
|
function pricePerSlotPerSecond(
|
||||||
Ask memory ask
|
Ask memory ask
|
||||||
) internal pure returns (TokensPerSecond) {
|
) internal pure returns (TokensPerSecond) {
|
||||||
|
|||||||
@ -5,6 +5,9 @@ import "./Marketplace.sol";
|
|||||||
|
|
||||||
// exposes internal functions of Marketplace for testing
|
// exposes internal functions of Marketplace for testing
|
||||||
contract TestMarketplace is Marketplace {
|
contract TestMarketplace is Marketplace {
|
||||||
|
using VaultHelpers for RequestId;
|
||||||
|
using VaultHelpers for Vault;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
MarketplaceConfig memory config,
|
MarketplaceConfig memory config,
|
||||||
Vault vault,
|
Vault vault,
|
||||||
@ -15,8 +18,11 @@ contract TestMarketplace is Marketplace {
|
|||||||
_forciblyFreeSlot(slotId);
|
_forciblyFreeSlot(slotId);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getSlotCollateral(SlotId slotId) public view returns (uint256) {
|
function getSlotBalance(SlotId slotId) public view returns (uint256) {
|
||||||
return _slots[slotId].currentCollateral;
|
Slot storage slot = _slots[slotId];
|
||||||
|
FundId fund = slot.requestId.asFundId();
|
||||||
|
AccountId account = vault().hostAccount(slot.host, slot.slotIndex);
|
||||||
|
return vault().getBalance(fund, account);
|
||||||
}
|
}
|
||||||
|
|
||||||
function challengeToFieldElement(
|
function challengeToFieldElement(
|
||||||
|
|||||||
69
contracts/marketplace/Collateral.sol
Normal file
69
contracts/marketplace/Collateral.sol
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
pragma solidity 0.8.28;
|
||||||
|
|
||||||
|
import "../Configuration.sol";
|
||||||
|
import "../Requests.sol";
|
||||||
|
|
||||||
|
library Collateral {
|
||||||
|
using Collateral for Request;
|
||||||
|
using Collateral for CollateralConfig;
|
||||||
|
|
||||||
|
function checkCorrectness(
|
||||||
|
CollateralConfig memory configuration
|
||||||
|
) internal pure {
|
||||||
|
require(
|
||||||
|
configuration.repairRewardPercentage <= 100,
|
||||||
|
Marketplace_RepairRewardPercentageTooHigh()
|
||||||
|
);
|
||||||
|
require(
|
||||||
|
configuration.slashPercentage <= 100,
|
||||||
|
Marketplace_SlashPercentageTooHigh()
|
||||||
|
);
|
||||||
|
require(
|
||||||
|
configuration.maxNumberOfSlashes * configuration.slashPercentage <= 100,
|
||||||
|
Marketplace_MaximumSlashingTooHigh()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function collateralPerSlot(
|
||||||
|
Request storage request
|
||||||
|
) internal view returns (uint128) {
|
||||||
|
return request.ask.collateralPerByte * request.ask.slotSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
function slashAmount(
|
||||||
|
CollateralConfig storage configuration,
|
||||||
|
uint128 collateral
|
||||||
|
) internal view returns (uint128) {
|
||||||
|
return (collateral * configuration.slashPercentage) / 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
function repairReward(
|
||||||
|
CollateralConfig storage configuration,
|
||||||
|
uint128 collateral
|
||||||
|
) internal view returns (uint128) {
|
||||||
|
return (collateral * configuration.repairRewardPercentage) / 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
function validatorReward(
|
||||||
|
CollateralConfig storage configuration,
|
||||||
|
uint128 slashed
|
||||||
|
) internal view returns (uint128) {
|
||||||
|
return (slashed * configuration.validatorRewardPercentage) / 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
function designatedCollateral(
|
||||||
|
CollateralConfig storage configuration,
|
||||||
|
uint128 collateral
|
||||||
|
) internal view returns (uint128) {
|
||||||
|
uint8 slashes = configuration.maxNumberOfSlashes;
|
||||||
|
uint128 slashing = configuration.slashAmount(collateral);
|
||||||
|
uint128 validation = slashes * configuration.validatorReward(slashing);
|
||||||
|
uint128 repair = configuration.repairReward(collateral);
|
||||||
|
return collateral - validation - repair;
|
||||||
|
}
|
||||||
|
|
||||||
|
error Marketplace_RepairRewardPercentageTooHigh();
|
||||||
|
error Marketplace_SlashPercentageTooHigh();
|
||||||
|
error Marketplace_MaximumSlashingTooHigh();
|
||||||
|
}
|
||||||
@ -1166,18 +1166,16 @@ describe("Marketplace", function () {
|
|||||||
await waitUntilProofIsRequired(id)
|
await waitUntilProofIsRequired(id)
|
||||||
let missedPeriod = periodOf(await currentTime())
|
let missedPeriod = periodOf(await currentTime())
|
||||||
await advanceTime(period + 1)
|
await advanceTime(period + 1)
|
||||||
|
|
||||||
|
const startBalance = await marketplace.getSlotBalance(id)
|
||||||
|
await setNextBlockTimestamp(await currentTime())
|
||||||
await marketplace.markProofAsMissing(id, missedPeriod)
|
await marketplace.markProofAsMissing(id, missedPeriod)
|
||||||
|
const endBalance = await marketplace.getSlotBalance(id)
|
||||||
|
|
||||||
const collateral = collateralPerSlot(request)
|
const collateral = collateralPerSlot(request)
|
||||||
const expectedBalance = Math.round(
|
const expectedSlash = Math.round((collateral * slashPercentage) / 100)
|
||||||
(collateral * (100 - slashPercentage)) / 100
|
|
||||||
)
|
|
||||||
|
|
||||||
expect(
|
expect(endBalance).to.equal(startBalance - expectedSlash)
|
||||||
BigNumber.from(expectedBalance).eq(
|
|
||||||
await marketplace.getSlotCollateral(id)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it("rewards validator when marking proof as missing", async function () {
|
it("rewards validator when marking proof as missing", async function () {
|
||||||
@ -1211,62 +1209,30 @@ describe("Marketplace", function () {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
it("frees slot when collateral slashed below minimum threshold", async function () {
|
describe("when slashing the maximum number of times", function () {
|
||||||
const collateral = collateralPerSlot(request)
|
beforeEach(async function () {
|
||||||
const minimum =
|
await waitUntilStarted(marketplace, request, proof, token)
|
||||||
collateral -
|
for (let i = 0; i < config.collateral.maxNumberOfSlashes; i++) {
|
||||||
(collateral *
|
await waitUntilProofIsRequired(slotId(slot))
|
||||||
config.collateral.maxNumberOfSlashes *
|
const missedPeriod = periodOf(await currentTime())
|
||||||
config.collateral.slashPercentage) /
|
await advanceTime(period + 1)
|
||||||
100
|
await marketplace.markProofAsMissing(slotId(slot), missedPeriod)
|
||||||
await waitUntilStarted(marketplace, request, proof, token)
|
}
|
||||||
while ((await marketplace.slotState(slotId(slot))) === SlotState.Filled) {
|
})
|
||||||
expect(await marketplace.getSlotCollateral(slotId(slot))).to.be.gt(
|
|
||||||
minimum
|
|
||||||
)
|
|
||||||
await waitUntilProofIsRequired(slotId(slot))
|
|
||||||
const missedPeriod = periodOf(await currentTime())
|
|
||||||
await advanceTime(period + 1)
|
|
||||||
await marketplace.markProofAsMissing(slotId(slot), missedPeriod)
|
|
||||||
}
|
|
||||||
expect(await marketplace.slotState(slotId(slot))).to.equal(
|
|
||||||
SlotState.Repair
|
|
||||||
)
|
|
||||||
expect(await marketplace.getSlotCollateral(slotId(slot))).to.be.lte(
|
|
||||||
minimum
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
it("free slot when minimum reached and resets missed proof counter", async function () {
|
it("sets the state to 'repair'", async function () {
|
||||||
const collateral = collateralPerSlot(request)
|
expect(await marketplace.slotState(slotId(slot))).to.equal(
|
||||||
const minimum =
|
SlotState.Repair
|
||||||
collateral -
|
|
||||||
(collateral *
|
|
||||||
config.collateral.maxNumberOfSlashes *
|
|
||||||
config.collateral.slashPercentage) /
|
|
||||||
100
|
|
||||||
await waitUntilStarted(marketplace, request, proof, token)
|
|
||||||
let missedProofs = 0
|
|
||||||
while ((await marketplace.slotState(slotId(slot))) === SlotState.Filled) {
|
|
||||||
expect(await marketplace.getSlotCollateral(slotId(slot))).to.be.gt(
|
|
||||||
minimum
|
|
||||||
)
|
)
|
||||||
await waitUntilProofIsRequired(slotId(slot))
|
})
|
||||||
const missedPeriod = periodOf(await currentTime())
|
|
||||||
await advanceTime(period + 1)
|
it("burns the balance", async function () {
|
||||||
expect(await marketplace.missingProofs(slotId(slot))).to.equal(
|
expect(await marketplace.getSlotBalance(slotId(slot))).to.equal(0)
|
||||||
missedProofs
|
})
|
||||||
)
|
|
||||||
await marketplace.markProofAsMissing(slotId(slot), missedPeriod)
|
it("resets missed proof counter", async function () {
|
||||||
missedProofs += 1
|
expect(await marketplace.missingProofs(slotId(slot))).to.equal(0)
|
||||||
}
|
})
|
||||||
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
|
|
||||||
)
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user