marketplace: burn tokens in vault when slashing

- move all collateral calculatons to separate library
This commit is contained in:
Mark Spanbroek 2025-02-27 08:23:56 +01:00
parent 639466662d
commit bddbd02f02
5 changed files with 121 additions and 124 deletions

View File

@ -14,11 +14,9 @@ import "./StateRetrieval.sol";
import "./Endian.sol";
import "./Groth16.sol";
import "./marketplace/VaultHelpers.sol";
import "./marketplace/Collateral.sol";
contract Marketplace is SlotReservations, Proofs, StateRetrieval, Endian {
error Marketplace_RepairRewardPercentageTooHigh();
error Marketplace_SlashPercentageTooHigh();
error Marketplace_MaximumSlashingTooHigh();
error Marketplace_InvalidExpiry();
error Marketplace_InvalidMaxSlotLoss();
error Marketplace_InsufficientSlots();
@ -49,6 +47,8 @@ contract Marketplace is SlotReservations, Proofs, StateRetrieval, Endian {
using VaultHelpers for Vault;
using VaultHelpers for RequestId;
using VaultHelpers for Request;
using Collateral for Request;
using Collateral for CollateralConfig;
using Timestamps for Timestamp;
using Tokens for TokensPerSecond;
@ -75,14 +75,6 @@ contract Marketplace is SlotReservations, Proofs, StateRetrieval, Endian {
/// based on time they actually host the content
Timestamp filledAt;
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
address host;
}
@ -98,18 +90,7 @@ contract Marketplace is SlotReservations, Proofs, StateRetrieval, Endian {
IGroth16Verifier verifier
) SlotReservations(config.reservations) Proofs(config.proofs, verifier) {
_vault = vault_;
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.collateral.checkCorrectness();
_config = config;
}
@ -125,10 +106,6 @@ contract Marketplace is SlotReservations, Proofs, StateRetrieval, Endian {
return _vault;
}
function currentCollateral(SlotId slotId) public view returns (uint256) {
return _slots[slotId].currentCollateral;
}
function requestStorage(Request calldata request) public {
RequestId id = request.id();
@ -226,17 +203,15 @@ contract Marketplace is SlotReservations, Proofs, StateRetrieval, Endian {
context.slotsFilled += 1;
// Collect collateral
uint128 collateralAmount;
uint128 collateralPerSlot = request.ask.collateralPerSlot();
uint128 collateralAmount = request.collateralPerSlot();
uint128 designatedAmount = _config.collateral.designatedCollateral(
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 =
collateralPerSlot -
((collateralPerSlot * _config.collateral.repairRewardPercentage) / 100);
} else {
collateralAmount = collateralPerSlot;
collateralAmount -= _config.collateral.repairReward(collateralAmount);
}
FundId fund = requestId.asFundId();
@ -245,10 +220,9 @@ contract Marketplace is SlotReservations, Proofs, StateRetrieval, Endian {
TokensPerSecond rate = request.ask.pricePerSlotPerSecond();
_transferToVault(slot.host, fund, hostAccount, collateralAmount);
_vault.designate(fund, hostAccount, designatedAmount);
_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);
slot.state = SlotState.Filled;
@ -343,23 +317,16 @@ contract Marketplace is SlotReservations, Proofs, StateRetrieval, Endian {
Slot storage slot = _slots[slotId];
Request storage request = _requests[slot.requestId];
uint128 slashedAmount = (request.ask.collateralPerSlot() *
_config.collateral.slashPercentage) / 100;
uint128 validatorRewardAmount = (slashedAmount *
_config.collateral.validatorRewardPercentage) / 100;
uint128 collateral = request.collateralPerSlot();
uint128 slashedAmount = _config.collateral.slashAmount(collateral);
uint128 validatorReward = _config.collateral.validatorReward(slashedAmount);
FundId fund = slot.requestId.asFundId();
AccountId hostAccount = _vault.hostAccount(slot.host, slot.slotIndex);
AccountId validatorAccount = _vault.validatorAccount(msg.sender);
_vault.transfer(
fund,
hostAccount,
validatorAccount,
validatorRewardAmount
);
_vault.transfer(fund, hostAccount, validatorAccount, validatorReward);
_vault.burnDesignated(fund, hostAccount, slashedAmount - validatorReward);
slot.currentCollateral -= slashedAmount;
if (missingProofs(slotId) >= _config.collateral.maxNumberOfSlashes) {
// When the number of slashings is at or above the allowed amount,
// free the slot.
@ -367,13 +334,7 @@ contract Marketplace is SlotReservations, Proofs, StateRetrieval, Endian {
}
}
/**
* @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.
*/
/// Abandons the slot, burns all associated tokens
function _forciblyFreeSlot(SlotId slotId) internal {
Slot storage slot = _slots[slotId];
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
slot.state = SlotState.Repair;
slot.filledAt = Timestamp.wrap(0);
slot.currentCollateral = 0;
slot.host = address(0);
context.slotsFilled -= 1;
emit SlotFreed(requestId, slot.slotIndex);

View File

@ -49,10 +49,6 @@ enum SlotState {
}
library AskHelpers {
function collateralPerSlot(Ask memory ask) internal pure returns (uint128) {
return ask.collateralPerByte * ask.slotSize;
}
function pricePerSlotPerSecond(
Ask memory ask
) internal pure returns (TokensPerSecond) {

View File

@ -5,6 +5,9 @@ import "./Marketplace.sol";
// exposes internal functions of Marketplace for testing
contract TestMarketplace is Marketplace {
using VaultHelpers for RequestId;
using VaultHelpers for Vault;
constructor(
MarketplaceConfig memory config,
Vault vault,
@ -15,8 +18,11 @@ contract TestMarketplace is Marketplace {
_forciblyFreeSlot(slotId);
}
function getSlotCollateral(SlotId slotId) public view returns (uint256) {
return _slots[slotId].currentCollateral;
function getSlotBalance(SlotId slotId) public view returns (uint256) {
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(

View 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();
}

View File

@ -1166,18 +1166,16 @@ describe("Marketplace", function () {
await waitUntilProofIsRequired(id)
let missedPeriod = periodOf(await currentTime())
await advanceTime(period + 1)
const startBalance = await marketplace.getSlotBalance(id)
await setNextBlockTimestamp(await currentTime())
await marketplace.markProofAsMissing(id, missedPeriod)
const endBalance = await marketplace.getSlotBalance(id)
const collateral = collateralPerSlot(request)
const expectedBalance = Math.round(
(collateral * (100 - slashPercentage)) / 100
)
const expectedSlash = Math.round((collateral * slashPercentage) / 100)
expect(
BigNumber.from(expectedBalance).eq(
await marketplace.getSlotCollateral(id)
)
)
expect(endBalance).to.equal(startBalance - expectedSlash)
})
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 () {
const collateral = collateralPerSlot(request)
const minimum =
collateral -
(collateral *
config.collateral.maxNumberOfSlashes *
config.collateral.slashPercentage) /
100
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
)
})
describe("when slashing the maximum number of times", function () {
beforeEach(async function () {
await waitUntilStarted(marketplace, request, proof, token)
for (let i = 0; i < config.collateral.maxNumberOfSlashes; i++) {
await waitUntilProofIsRequired(slotId(slot))
const missedPeriod = periodOf(await currentTime())
await advanceTime(period + 1)
await marketplace.markProofAsMissing(slotId(slot), missedPeriod)
}
})
it("free slot when minimum reached and resets missed proof counter", async function () {
const collateral = collateralPerSlot(request)
const minimum =
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
it("sets the state to 'repair'", async function () {
expect(await marketplace.slotState(slotId(slot))).to.equal(
SlotState.Repair
)
await waitUntilProofIsRequired(slotId(slot))
const missedPeriod = periodOf(await currentTime())
await advanceTime(period + 1)
expect(await marketplace.missingProofs(slotId(slot))).to.equal(
missedProofs
)
await marketplace.markProofAsMissing(slotId(slot), missedPeriod)
missedProofs += 1
}
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
)
})
it("burns the balance", async function () {
expect(await marketplace.getSlotBalance(slotId(slot))).to.equal(0)
})
it("resets missed proof counter", async function () {
expect(await marketplace.missingProofs(slotId(slot))).to.equal(0)
})
})
})