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 "./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);

View File

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

View File

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

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) 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
)
}) })
}) })