mirror of
https://github.com/logos-storage/logos-storage-contracts-eth.git
synced 2026-01-03 22:03:08 +00:00
feat: protocol fee
This commit is contained in:
parent
129e82f707
commit
da59b7f31c
@ -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) {
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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))
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -20,6 +20,7 @@ const exampleConfiguration = () => ({
|
||||
maxReservations: 3,
|
||||
},
|
||||
requestDurationLimit: 60 * 60 * 24 * 30, // 30 days
|
||||
protocolFeePermille: 5,
|
||||
})
|
||||
|
||||
const exampleRequest = async () => {
|
||||
|
||||
@ -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 }
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user