feat: protocol fee

This commit is contained in:
Adam Uhlíř 2025-03-24 17:36:21 +01:00
parent 129e82f707
commit da59b7f31c
No known key found for this signature in database
GPG Key ID: 1D17A9E81F76155B
7 changed files with 114 additions and 34 deletions

View File

@ -20,7 +20,8 @@ const DEFAULT_CONFIGURATION = {
reservations: {
maxReservations: 3,
},
requestDurationLimit: 60*60*24*30 // 30 days
requestDurationLimit: 60*60*24*30, // 30 days
protocolFeePermille: 5, // dictates how much of the request price (defined by its ask) should go to protocol fee that is burned; specified in a per mille
}
function loadConfiguration(name) {

View File

@ -4,6 +4,7 @@ module.exports = {
maxNumberOfSlashes: 2,
slashPercentage: 20,
validatorRewardPercentage: 20, // percentage of the slashed amount going to the validators
protocolFeePermille: 5, // dictates how much of the request price (defined by its ask) should go to protocol fee that is burned; specified in a per mille
},
proofs: {
// period has to be less than downtime * blocktime

View File

@ -9,6 +9,7 @@ struct MarketplaceConfig {
ProofConfig proofs;
SlotReservationsConfig reservations;
Duration requestDurationLimit;
uint16 protocolFeePermille; // dictates how much of the request price (defined by its ask) should go to protocol fee that is burned; specified in a per mille
}
struct CollateralConfig {

View File

@ -37,6 +37,7 @@ contract Marketplace is SlotReservations, Proofs, StateRetrieval, Endian {
error Marketplace_SlotIsFree();
error Marketplace_ReservationRequired();
error Marketplace_DurationExceedsLimit();
error Marketplace_ProtocolFeePermilleTooHigh();
using SafeERC20 for IERC20;
using EnumerableSet for EnumerableSet.Bytes32Set;
@ -84,6 +85,12 @@ contract Marketplace is SlotReservations, Proofs, StateRetrieval, Endian {
) SlotReservations(config.reservations) Proofs(config.proofs, verifier) {
_vault = vault_;
config.collateral.checkCorrectness();
require(
config.protocolFeePermille <= 1000,
Marketplace_ProtocolFeePermilleTooHigh()
);
_config = config;
}
@ -149,6 +156,8 @@ contract Marketplace is SlotReservations, Proofs, StateRetrieval, Endian {
TokensPerSecond pricePerSecond = request.ask.pricePerSecond();
uint128 price = pricePerSecond.accumulate(request.ask.duration);
_collectProtocolFee(request.client, request.ask);
FundId fund = id.asFundId();
AccountId account = _vault.clientAccount(request.client);
_vault.lock(fund, expiresAt, endsAt);
@ -158,6 +167,15 @@ contract Marketplace is SlotReservations, Proofs, StateRetrieval, Endian {
emit StorageRequested(id, request.ask, expiresAt);
}
/**
* Calculates the protocol fee, which is then burned.
* @param ask Request's ask
*/
function _collectProtocolFee(address from, Ask memory ask) private {
uint256 fee = protocolFeeForRequestAsk(ask);
_vault.getToken().safeTransferFrom(from, address(0xdead), fee);
}
/**
* @notice Fills a slot. Reverts if an invalid proof of the slot data is
provided.
@ -466,6 +484,13 @@ contract Marketplace is SlotReservations, Proofs, StateRetrieval, Endian {
return _requestContexts[requestId].expiresAt;
}
function protocolFeeForRequestAsk(Ask memory ask) public view returns(uint256) {
TokensPerSecond pricePerSecond = ask.pricePerSecond();
uint128 requestPrice = pricePerSecond.accumulate(ask.duration);
return (requestPrice/1000) * _config.protocolFeePermille;
}
function getHost(SlotId slotId) public view returns (address) {
return _slots[slotId].host;
}

View File

@ -25,8 +25,10 @@ const {
} = require("./marketplace")
const {
maxPrice,
maxPriceWithProtocolFee,
pricePerSlotPerSecond,
payoutForDuration,
protocolFee
} = require("./price")
const { collateralPerSlot, repairReward } = require("./collateral")
const {
@ -91,6 +93,14 @@ describe("Marketplace constructor", function () {
Marketplace.deploy(config, vault.address, verifier.address)
).to.be.revertedWith("Marketplace_MaximumSlashingTooHigh")
})
it("should reject when protocol fee permille exceeds 1000%%", async () => {
config.protocolFeePermille = 1001
await expect(
Marketplace.deploy(config, vault.address, verifier.address)
).to.be.revertedWith("Marketplace_ProtocolFeePermilleTooHigh")
})
})
describe("Marketplace", function () {
@ -158,7 +168,7 @@ describe("Marketplace", function () {
})
it("emits event when storage is requested", async function () {
await token.approve(marketplace.address, maxPrice(request))
await token.approve(marketplace.address, maxPriceWithProtocolFee(request, config))
const now = await currentTime()
await setNextBlockTimestamp(now)
const expectedExpiry = now + request.expiry
@ -168,7 +178,7 @@ describe("Marketplace", function () {
})
it("allows retrieval of request details", async function () {
await token.approve(marketplace.address, maxPrice(request))
await token.approve(marketplace.address, maxPriceWithProtocolFee(request, config))
await marketplace.requestStorage(request)
const id = requestId(request)
expect(await marketplace.getRequest(id)).to.be.request(request)
@ -176,7 +186,7 @@ describe("Marketplace", function () {
it("rejects request with invalid client address", async function () {
let invalid = { ...request, client: host.address }
await token.approve(marketplace.address, maxPrice(invalid))
await token.approve(marketplace.address, maxPriceWithProtocolFee(invalid, config))
await expect(marketplace.requestStorage(invalid)).to.be.revertedWith(
"Marketplace_InvalidClientAddress"
)
@ -191,7 +201,7 @@ describe("Marketplace", function () {
})
it("rejects request with insufficient payment", async function () {
let insufficient = maxPrice(request) - 1
let insufficient = maxPriceWithProtocolFee(request, config) - 1
await token.approve(marketplace.address, insufficient)
await expect(marketplace.requestStorage(request)).to.be.revertedWith(
"ERC20InsufficientAllowance"
@ -199,7 +209,7 @@ describe("Marketplace", function () {
})
it("rejects request when expiry out of bounds", async function () {
await token.approve(marketplace.address, maxPrice(request))
await token.approve(marketplace.address, maxPriceWithProtocolFee(request, config))
request.expiry = request.ask.duration + 1
await expect(marketplace.requestStorage(request)).to.be.revertedWith(
@ -227,7 +237,7 @@ describe("Marketplace", function () {
})
it("rejects resubmission of request", async function () {
await token.approve(marketplace.address, maxPrice(request) * 2)
await token.approve(marketplace.address, maxPriceWithProtocolFee(request, config) * 2)
await marketplace.requestStorage(request)
await expect(marketplace.requestStorage(request)).to.be.revertedWith(
"Marketplace_RequestAlreadyExists"
@ -276,7 +286,7 @@ describe("Marketplace", function () {
describe("filling a slot with collateral", function () {
beforeEach(async function () {
switchAccount(client)
await token.approve(marketplace.address, maxPrice(request))
await token.approve(marketplace.address, maxPriceWithProtocolFee(request, config))
await marketplace.requestStorage(request)
switchAccount(host)
await token.approve(marketplace.address, collateralPerSlot(request))
@ -356,7 +366,7 @@ describe("Marketplace", function () {
it("is rejected when request is cancelled", async function () {
switchAccount(client)
let expired = { ...request, expiry: hours(1) + 1 }
await token.approve(marketplace.address, maxPrice(request))
await token.approve(marketplace.address, maxPriceWithProtocolFee(request, config))
await marketplace.requestStorage(expired)
await waitUntilCancelled(marketplace, expired)
switchAccount(host)
@ -395,7 +405,7 @@ describe("Marketplace", function () {
marketplace.address,
collateralPerSlot(request) * lastSlot
)
await token.approve(marketplace.address, maxPrice(request) * lastSlot)
await token.approve(marketplace.address, maxPriceWithProtocolFee(request, config) * lastSlot)
for (let i = 0; i <= lastSlot; i++) {
await marketplace.reserveSlot(slot.request, i)
await marketplace.fillSlot(slot.request, i, proof)
@ -415,7 +425,7 @@ describe("Marketplace", function () {
describe("filling slot without collateral", function () {
beforeEach(async function () {
switchAccount(client)
await token.approve(marketplace.address, maxPrice(request))
await token.approve(marketplace.address, maxPriceWithProtocolFee(request, config))
await marketplace.requestStorage(request)
switchAccount(host)
})
@ -442,7 +452,7 @@ describe("Marketplace", function () {
describe("submitting proofs when slot is filled", function () {
beforeEach(async function () {
switchAccount(client)
await token.approve(marketplace.address, maxPrice(request))
await token.approve(marketplace.address, maxPriceWithProtocolFee(request, config))
await marketplace.requestStorage(request)
switchAccount(host)
const collateral = collateralPerSlot(request)
@ -480,7 +490,7 @@ describe("Marketplace", function () {
var requestTime
beforeEach(async function () {
switchAccount(client)
await token.approve(marketplace.address, maxPrice(request))
await token.approve(marketplace.address, maxPriceWithProtocolFee(request, config))
await marketplace.requestStorage(request)
requestTime = await currentTime()
switchAccount(host)
@ -524,7 +534,7 @@ describe("Marketplace", function () {
id = slotId(slot)
switchAccount(client)
await token.approve(marketplace.address, maxPrice(request))
await token.approve(marketplace.address, maxPriceWithProtocolFee(request, config))
await marketplace.requestStorage(request)
switchAccount(host)
const collateral = collateralPerSlot(request)
@ -563,7 +573,7 @@ describe("Marketplace", function () {
describe("paying out a slot", function () {
beforeEach(async function () {
switchAccount(client)
await token.approve(marketplace.address, maxPrice(request))
await token.approve(marketplace.address, maxPriceWithProtocolFee(request, config))
await marketplace.requestStorage(request)
switchAccount(host)
const collateral = collateralPerSlot(request)
@ -643,7 +653,7 @@ describe("Marketplace", function () {
describe("fulfilling a request", function () {
beforeEach(async function () {
switchAccount(client)
await token.approve(marketplace.address, maxPrice(request))
await token.approve(marketplace.address, maxPriceWithProtocolFee(request, config))
await marketplace.requestStorage(request)
switchAccount(host)
const collateral = collateralPerSlot(request)
@ -699,7 +709,7 @@ describe("Marketplace", function () {
describe("withdrawing funds", function () {
beforeEach(async function () {
switchAccount(client)
await token.approve(marketplace.address, maxPrice(request))
await token.approve(marketplace.address, maxPriceWithProtocolFee(request, config))
await marketplace.requestStorage(request)
// wait a bit, so that there are funds for the client to withdraw
@ -784,13 +794,13 @@ describe("Marketplace", function () {
const startBalance = await token.balanceOf(client.address)
await marketplace.withdrawFunds(slot.request)
const endBalance = await token.balanceOf(client.address)
expect(endBalance - startBalance).to.equal(maxPrice(request))
expect(endBalance - startBalance).to.equal(maxPriceWithProtocolFee(request, config) - protocolFee(request, config))
})
it("refunds the client for the remaining time when request fails", async function () {
await waitUntilStarted(marketplace, request, proof, token)
await waitUntilFailed(marketplace, request)
const failedAt = await currentTime()
const startedAt = await currentTime()
await waitUntilFinished(marketplace, requestId(request))
const finishedAt = await currentTime()
@ -800,10 +810,7 @@ describe("Marketplace", function () {
await marketplace.withdrawFunds(slot.request)
const endBalance = await token.balanceOf(client.address)
const expectedRefund =
(finishedAt - failedAt) *
request.ask.slots *
pricePerSlotPerSecond(request)
const expectedRefund = payoutForDuration(request, startedAt, finishedAt) - protocolFee(request, config)
expect(endBalance - startBalance).to.be.gte(expectedRefund)
})
@ -816,8 +823,7 @@ describe("Marketplace", function () {
await setNextBlockTimestamp(filledAt)
await marketplace.fillSlot(slot.request, slot.index, proof)
await waitUntilCancelled(marketplace, request)
const expectedPartialHostReward =
(expiresAt - filledAt) * pricePerSlotPerSecond(request)
const expectedPartialHostReward = payoutForDuration(request, filledAt, expiresAt)
switchAccount(client)
const startBalance = await token.balanceOf(client.address)
@ -845,7 +851,7 @@ describe("Marketplace", function () {
const refund = payoutForDuration(request, freedAt, requestEnd)
const reward = repairReward(config, collateralPerSlot(request))
expect(endBalance - startBalance).to.equal(
maxPrice(request) - hostPayouts + refund + reward
maxPrice(request, config) - hostPayouts + refund + reward
)
})
})
@ -855,7 +861,7 @@ describe("Marketplace", function () {
beforeEach(async function () {
switchAccount(client)
await token.approve(marketplace.address, maxPrice(request))
await token.approve(marketplace.address, maxPriceWithProtocolFee(request, config))
await marketplace.requestStorage(request)
switchAccount(host)
const collateral = collateralPerSlot(request)
@ -929,7 +935,7 @@ describe("Marketplace", function () {
;({ periodOf, periodEnd } = periodic(period))
switchAccount(client)
await token.approve(marketplace.address, maxPrice(request))
await token.approve(marketplace.address, maxPriceWithProtocolFee(request, config))
await marketplace.requestStorage(request)
switchAccount(host)
const collateral = collateralPerSlot(request)
@ -999,7 +1005,7 @@ describe("Marketplace", function () {
describe("slot probability", function () {
beforeEach(async function () {
switchAccount(client)
await token.approve(marketplace.address, maxPrice(request))
await token.approve(marketplace.address, maxPriceWithProtocolFee(request, config))
await marketplace.requestStorage(request)
switchAccount(host)
const collateral = collateralPerSlot(request)
@ -1028,7 +1034,7 @@ describe("Marketplace", function () {
;({ periodOf, periodEnd } = periodic(period))
switchAccount(client)
await token.approve(marketplace.address, maxPrice(request))
await token.approve(marketplace.address, maxPriceWithProtocolFee(request, config))
await marketplace.requestStorage(request)
switchAccount(host)
const collateral = collateralPerSlot(request)
@ -1113,7 +1119,7 @@ describe("Marketplace", function () {
;({ periodOf, periodEnd } = periodic(period))
switchAccount(client)
await token.approve(marketplace.address, maxPrice(request))
await token.approve(marketplace.address, maxPriceWithProtocolFee(request, config))
await marketplace.requestStorage(request)
switchAccount(host)
const collateral = collateralPerSlot(request)
@ -1228,7 +1234,7 @@ describe("Marketplace", function () {
const collateral = collateralPerSlot(request)
await token.approve(marketplace.address, collateral)
switchAccount(client)
await token.approve(marketplace.address, maxPrice(request))
await token.approve(marketplace.address, maxPriceWithProtocolFee(request, config))
})
it("adds request to list when requesting storage", async function () {
@ -1272,7 +1278,7 @@ describe("Marketplace", function () {
describe("list of active slots", function () {
beforeEach(async function () {
switchAccount(client)
await token.approve(marketplace.address, maxPrice(request))
await token.approve(marketplace.address, maxPriceWithProtocolFee(request, config))
await marketplace.requestStorage(request)
switchAccount(host)
const collateral = collateralPerSlot(request)
@ -1344,4 +1350,34 @@ describe("Marketplace", function () {
expect(await marketplace.mySlots()).to.not.contain(slotId(slot))
})
})
describe("protocol fee", function () {
const dead = "0x000000000000000000000000000000000000dead"
it("is burned when storage request is created", async function () {
switchAccount(client)
let priceWithProtocolFee = maxPriceWithProtocolFee(request, config)
await token.approve(marketplace.address, priceWithProtocolFee)
const startBalance = await token.balanceOf(client.address)
const startDeadBalance = await token.balanceOf(dead)
await expect(marketplace.requestStorage(request))
.to.emit(marketplace, "StorageRequested")
const endBalance = await token.balanceOf(client.address)
const endDeadBalance = await token.balanceOf(dead)
expect(startBalance - endBalance).to.equal(maxPriceWithProtocolFee(request, config))
expect(endDeadBalance - startDeadBalance).to.equal(protocolFee(request, config))
})
it("is not returned when request is cancelled", async function () {
switchAccount(client)
await token.approve(marketplace.address, maxPriceWithProtocolFee(request, config))
await marketplace.requestStorage(request)
await waitUntilCancelled(marketplace, request)
const startBalance = await token.balanceOf(client.address)
await marketplace.withdrawFunds(slot.request)
const endBalance = await token.balanceOf(client.address)
expect(endBalance - startBalance).to.equal(maxPriceWithProtocolFee(request, config) - protocolFee(request, config))
})
})
})

View File

@ -20,6 +20,7 @@ const exampleConfiguration = () => ({
maxReservations: 3,
},
requestDurationLimit: 60 * 60 * 24 * 30, // 30 days
protocolFeePermille: 5,
})
const exampleRequest = async () => {

View File

@ -2,14 +2,29 @@ function pricePerSlotPerSecond(request) {
return request.ask.pricePerBytePerSecond * request.ask.slotSize
}
function protocolFee(request, config) {
let requestPrice = request.ask.slots * request.ask.duration * pricePerSlotPerSecond(request)
return (requestPrice / 1000) * config.protocolFeePermille
}
function maxPrice(request) {
return (
request.ask.slots * request.ask.duration * pricePerSlotPerSecond(request)
)
}
function maxPriceWithProtocolFee(request, config) {
return maxPrice(request) + protocolFee(request, config)
}
function maxPriceWithProtocolFee(request, config) {
let requestPrice = request.ask.slots * request.ask.duration * pricePerSlotPerSecond(request)
let protocolFee = (requestPrice / 1000) * config.protocolFeePermille
return requestPrice + protocolFee
}
function payoutForDuration(request, start, end) {
return (end - start) * pricePerSlotPerSecond(request)
}
module.exports = { maxPrice, pricePerSlotPerSecond, payoutForDuration }
module.exports = { maxPrice, maxPriceWithProtocolFee, protocolFee, pricePerSlotPerSecond, payoutForDuration }