mirror of
https://github.com/logos-storage/logos-storage-contracts-eth.git
synced 2026-01-05 23:03:12 +00:00
marketplace: use vault in marketplace
This commit is contained in:
parent
8b40f63693
commit
8df557801c
@ -13,6 +13,7 @@ import "./SlotReservations.sol";
|
||||
import "./StateRetrieval.sol";
|
||||
import "./Endian.sol";
|
||||
import "./Groth16.sol";
|
||||
import "./marketplace/VaultHelpers.sol";
|
||||
|
||||
contract Marketplace is SlotReservations, Proofs, StateRetrieval, Endian {
|
||||
error Marketplace_RepairRewardPercentageTooHigh();
|
||||
@ -46,6 +47,9 @@ contract Marketplace is SlotReservations, Proofs, StateRetrieval, Endian {
|
||||
using EnumerableSet for EnumerableSet.AddressSet;
|
||||
using Requests for Request;
|
||||
using AskHelpers for Ask;
|
||||
using VaultHelpers for Vault;
|
||||
using VaultHelpers for RequestId;
|
||||
using VaultHelpers for Request;
|
||||
|
||||
Vault private immutable _vault;
|
||||
MarketplaceConfig private _config;
|
||||
@ -173,10 +177,18 @@ contract Marketplace is SlotReservations, Proofs, StateRetrieval, Endian {
|
||||
|
||||
_addToMyRequests(request.client, id);
|
||||
|
||||
uint256 amount = request.maxPrice();
|
||||
uint128 amount = uint128(request.maxPrice());
|
||||
_requestContexts[id].fundsToReturnToClient = amount;
|
||||
_marketplaceTotals.received += amount;
|
||||
token().safeTransferFrom(msg.sender, address(this), amount);
|
||||
|
||||
FundId fund = id.asFundId();
|
||||
AccountId account = _vault.clientAccount(request.client);
|
||||
_vault.lock(
|
||||
fund,
|
||||
Timestamp.wrap(uint40(_requestContexts[id].expiresAt)),
|
||||
Timestamp.wrap(uint40(_requestContexts[id].endsAt))
|
||||
);
|
||||
_transferToVault(request.client, fund, account, amount);
|
||||
|
||||
emit StorageRequested(id, request.ask, _requestContexts[id].expiresAt);
|
||||
}
|
||||
@ -236,7 +248,14 @@ contract Marketplace is SlotReservations, Proofs, StateRetrieval, Endian {
|
||||
} else {
|
||||
collateralAmount = collateralPerSlot;
|
||||
}
|
||||
token().safeTransferFrom(msg.sender, address(this), collateralAmount);
|
||||
|
||||
FundId fund = requestId.asFundId();
|
||||
AccountId clientAccount = _vault.clientAccount(request.client);
|
||||
AccountId hostAccount = _vault.hostAccount(slot.host, slotIndex);
|
||||
|
||||
_transferToVault(slot.host, fund, hostAccount, uint128(collateralAmount));
|
||||
_vault.flow(fund, clientAccount, hostAccount, request.slotPrice());
|
||||
|
||||
_marketplaceTotals.received += collateralAmount;
|
||||
slot.currentCollateral = collateralPerSlot; // Even if he has collateral discounted, he is operating with full collateral
|
||||
|
||||
@ -251,10 +270,22 @@ contract Marketplace is SlotReservations, Proofs, StateRetrieval, Endian {
|
||||
) {
|
||||
context.state = RequestState.Started;
|
||||
context.startedAt = uint64(block.timestamp);
|
||||
_vault.extendLock(fund, Timestamp.wrap(uint40(context.endsAt)));
|
||||
emit RequestFulfilled(requestId);
|
||||
}
|
||||
}
|
||||
|
||||
function _transferToVault(
|
||||
address from,
|
||||
FundId fund,
|
||||
AccountId account,
|
||||
uint128 amount
|
||||
) private {
|
||||
_vault.getToken().safeTransferFrom(from, address(this), amount);
|
||||
_vault.getToken().approve(address(_vault), amount);
|
||||
_vault.deposit(fund, account, amount);
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Frees a slot, paying out rewards and returning collateral for
|
||||
finished or cancelled requests to the host that has filled the slot.
|
||||
@ -328,7 +359,19 @@ contract Marketplace is SlotReservations, Proofs, StateRetrieval, Endian {
|
||||
uint256 validatorRewardAmount = (slashedAmount *
|
||||
_config.collateral.validatorRewardPercentage) / 100;
|
||||
_marketplaceTotals.sent += validatorRewardAmount;
|
||||
token().safeTransfer(msg.sender, validatorRewardAmount);
|
||||
|
||||
FundId fund = slot.requestId.asFundId();
|
||||
AccountId hostAccount = _vault.hostAccount(
|
||||
slot.host,
|
||||
slot.slotIndex
|
||||
);
|
||||
AccountId validatorAccount = _vault.validatorAccount(msg.sender);
|
||||
_vault.transfer(
|
||||
fund,
|
||||
hostAccount,
|
||||
validatorAccount,
|
||||
uint128(validatorRewardAmount)
|
||||
);
|
||||
|
||||
slot.currentCollateral -= slashedAmount;
|
||||
if (missingProofs(slotId) >= _config.collateral.maxNumberOfSlashes) {
|
||||
@ -354,6 +397,17 @@ contract Marketplace is SlotReservations, Proofs, StateRetrieval, Endian {
|
||||
// we keep correctly the track of the funds that needs to be returned at the end.
|
||||
context.fundsToReturnToClient += _slotPayout(requestId, slot.filledAt);
|
||||
|
||||
Request storage request = _requests[requestId];
|
||||
|
||||
FundId fund = requestId.asFundId();
|
||||
AccountId hostAccount = _vault.hostAccount(
|
||||
slot.host,
|
||||
slot.slotIndex
|
||||
);
|
||||
AccountId clientAccount = _vault.clientAccount(request.client);
|
||||
_vault.flow(fund, hostAccount, clientAccount, request.slotPrice());
|
||||
_vault.burnAccount(fund, hostAccount);
|
||||
|
||||
_removeFromMySlots(slot.host, slotId);
|
||||
delete _reservations[slotId]; // We purge all the reservations for the slot
|
||||
slot.state = SlotState.Repair;
|
||||
@ -364,14 +418,14 @@ contract Marketplace is SlotReservations, Proofs, StateRetrieval, Endian {
|
||||
emit SlotFreed(requestId, slot.slotIndex);
|
||||
_resetMissingProofs(slotId);
|
||||
|
||||
Request storage request = _requests[requestId];
|
||||
uint256 slotsLost = request.ask.slots - context.slotsFilled;
|
||||
if (
|
||||
slotsLost > request.ask.maxSlotLoss &&
|
||||
context.state == RequestState.Started
|
||||
) {
|
||||
context.state = RequestState.Failed;
|
||||
context.endsAt = uint64(block.timestamp) - 1;
|
||||
_vault.freezeFund(fund);
|
||||
|
||||
emit RequestFailed(requestId);
|
||||
}
|
||||
}
|
||||
@ -392,7 +446,9 @@ contract Marketplace is SlotReservations, Proofs, StateRetrieval, Endian {
|
||||
uint256 collateralAmount = slot.currentCollateral;
|
||||
_marketplaceTotals.sent += (payoutAmount + collateralAmount);
|
||||
slot.state = SlotState.Paid;
|
||||
token().safeTransfer(slot.host, payoutAmount + collateralAmount);
|
||||
FundId fund = requestId.asFundId();
|
||||
AccountId account = _vault.hostAccount(slot.host, slot.slotIndex);
|
||||
_vault.withdraw(fund, account);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -417,7 +473,9 @@ contract Marketplace is SlotReservations, Proofs, StateRetrieval, Endian {
|
||||
uint256 collateralAmount = slot.currentCollateral;
|
||||
_marketplaceTotals.sent += (payoutAmount + collateralAmount);
|
||||
slot.state = SlotState.Paid;
|
||||
token().safeTransfer(slot.host, payoutAmount + collateralAmount);
|
||||
FundId fund = requestId.asFundId();
|
||||
AccountId account = _vault.hostAccount(slot.host, slot.slotIndex);
|
||||
_vault.withdraw(fund, account);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -470,12 +528,20 @@ contract Marketplace is SlotReservations, Proofs, StateRetrieval, Endian {
|
||||
uint256 amount = context.fundsToReturnToClient;
|
||||
_marketplaceTotals.sent += amount;
|
||||
|
||||
token().safeTransfer(request.client, amount);
|
||||
FundId fund = requestId.asFundId();
|
||||
AccountId account = _vault.clientAccount(request.client);
|
||||
_vault.withdraw(fund, account);
|
||||
|
||||
// We zero out the funds tracking in order to prevent double-spends
|
||||
context.fundsToReturnToClient = 0;
|
||||
}
|
||||
|
||||
function withdrawByValidator(RequestId requestId) public {
|
||||
FundId fund = requestId.asFundId();
|
||||
AccountId account = _vault.validatorAccount(msg.sender);
|
||||
_vault.withdraw(fund, account);
|
||||
}
|
||||
|
||||
function getActiveSlot(
|
||||
SlotId slotId
|
||||
) public view slotIsNotFree(slotId) returns (ActiveSlot memory) {
|
||||
@ -510,7 +576,11 @@ contract Marketplace is SlotReservations, Proofs, StateRetrieval, Endian {
|
||||
|
||||
function requestEnd(RequestId requestId) public view returns (uint64) {
|
||||
RequestState state = requestState(requestId);
|
||||
if (state == RequestState.New || state == RequestState.Started) {
|
||||
if (
|
||||
state == RequestState.New ||
|
||||
state == RequestState.Started ||
|
||||
state == RequestState.Failed
|
||||
) {
|
||||
return _requestContexts[requestId].endsAt;
|
||||
}
|
||||
if (state == RequestState.Cancelled) {
|
||||
|
||||
53
contracts/marketplace/VaultHelpers.sol
Normal file
53
contracts/marketplace/VaultHelpers.sol
Normal file
@ -0,0 +1,53 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
pragma solidity 0.8.28;
|
||||
|
||||
import "../Requests.sol";
|
||||
import "../Vault.sol";
|
||||
|
||||
import "hardhat/console.sol";
|
||||
|
||||
library VaultHelpers {
|
||||
enum VaultRole {
|
||||
client,
|
||||
host,
|
||||
validator
|
||||
}
|
||||
|
||||
function clientAccount(
|
||||
Vault vault,
|
||||
address client
|
||||
) internal pure returns (AccountId) {
|
||||
bytes12 discriminator = bytes12(bytes1(uint8(VaultRole.client)));
|
||||
return vault.encodeAccountId(client, discriminator);
|
||||
}
|
||||
|
||||
function hostAccount(
|
||||
Vault vault,
|
||||
address host,
|
||||
uint64 slotIndex
|
||||
) internal pure returns (AccountId) {
|
||||
bytes12 role = bytes12(bytes1(uint8(VaultRole.host)));
|
||||
bytes12 index = bytes12(uint96(slotIndex));
|
||||
bytes12 discriminator = role | index;
|
||||
return vault.encodeAccountId(host, discriminator);
|
||||
}
|
||||
|
||||
function validatorAccount(
|
||||
Vault vault,
|
||||
address validator
|
||||
) internal pure returns (AccountId) {
|
||||
bytes12 discriminator = bytes12(bytes1(uint8(VaultRole.validator)));
|
||||
return vault.encodeAccountId(validator, discriminator);
|
||||
}
|
||||
|
||||
function asFundId(RequestId requestId) internal pure returns (FundId) {
|
||||
return FundId.wrap(RequestId.unwrap(requestId));
|
||||
}
|
||||
|
||||
function slotPrice(
|
||||
Request memory request
|
||||
) internal pure returns (TokensPerSecond) {
|
||||
uint256 price = request.ask.pricePerBytePerSecond * request.ask.slotSize;
|
||||
return TokensPerSecond.wrap(uint96(price));
|
||||
}
|
||||
}
|
||||
@ -6,6 +6,9 @@ import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
|
||||
import "./Accounts.sol";
|
||||
import "./Funds.sol";
|
||||
|
||||
/// Unique identifier for a fund, chosen by the controller
|
||||
type FundId is bytes32;
|
||||
|
||||
/// Records account balances and token flows. Accounts are separated into funds.
|
||||
/// Funds are kept separate between controllers.
|
||||
///
|
||||
@ -46,8 +49,6 @@ abstract contract VaultBase {
|
||||
|
||||
/// Represents a smart contract that can redistribute and burn tokens in funds
|
||||
type Controller is address;
|
||||
/// Unique identifier for a fund, chosen by the controller
|
||||
type FundId is bytes32;
|
||||
|
||||
/// Each controller has its own set of funds
|
||||
mapping(Controller => mapping(FundId => Fund)) private _funds;
|
||||
|
||||
@ -23,7 +23,11 @@ const {
|
||||
waitUntilSlotFailed,
|
||||
patchOverloads,
|
||||
} = require("./marketplace")
|
||||
const { maxPrice, pricePerSlotPerSecond } = require("./price")
|
||||
const {
|
||||
maxPrice,
|
||||
pricePerSlotPerSecond,
|
||||
payoutForDuration,
|
||||
} = require("./price")
|
||||
const { collateralPerSlot } = require("./collateral")
|
||||
const {
|
||||
snapshot,
|
||||
@ -500,15 +504,6 @@ describe("Marketplace", function () {
|
||||
).to.equal(requestTime + request.ask.duration)
|
||||
})
|
||||
|
||||
it("sets request end time to the past once failed", async function () {
|
||||
await waitUntilStarted(marketplace, request, proof, token)
|
||||
await waitUntilFailed(marketplace, request)
|
||||
const now = await currentTime()
|
||||
await expect(await marketplace.requestEnd(requestId(request))).to.be.eq(
|
||||
now - 1
|
||||
)
|
||||
})
|
||||
|
||||
it("sets request end time to the past once cancelled", async function () {
|
||||
await marketplace.reserveSlot(slot.request, slot.index)
|
||||
await marketplace.fillSlot(slot.request, slot.index, proof)
|
||||
@ -583,7 +578,7 @@ describe("Marketplace", function () {
|
||||
await token.approve(marketplace.address, collateral)
|
||||
})
|
||||
|
||||
it("finished request pays out reward based on time hosted", async function () {
|
||||
it("pays out finished request based on time hosted", async function () {
|
||||
// We are advancing the time because most of the slots will be filled somewhere
|
||||
// in the "expiry window" and not at its beginning. This is more "real" setup
|
||||
// and demonstrates the partial payout feature better.
|
||||
@ -601,7 +596,6 @@ describe("Marketplace", function () {
|
||||
await marketplace.freeSlot(slotId(slot))
|
||||
const endBalanceHost = await token.balanceOf(host.address)
|
||||
|
||||
expect(expectedPayouts[slot.index]).to.be.lt(maxPrice(request))
|
||||
const collateral = collateralPerSlot(request)
|
||||
expect(endBalanceHost - startBalanceHost).to.equal(
|
||||
expectedPayouts[slot.index] + collateral
|
||||
@ -805,18 +799,24 @@ describe("Marketplace", function () {
|
||||
expect(endBalance - startBalance).to.equal(maxPrice(request))
|
||||
})
|
||||
|
||||
it("withdraws full price for failed requests to the client", async function () {
|
||||
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()
|
||||
await waitUntilFinished(marketplace, requestId(request))
|
||||
const finishedAt = await currentTime()
|
||||
|
||||
switchAccount(client)
|
||||
|
||||
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))
|
||||
const expectedRefund =
|
||||
(finishedAt - failedAt) *
|
||||
request.ask.slots *
|
||||
pricePerSlotPerSecond(request)
|
||||
expect(endBalance - startBalance).to.be.gte(expectedRefund)
|
||||
})
|
||||
|
||||
it("withdraws to the client for cancelled requests lowered by hosts payout", async function () {
|
||||
@ -844,21 +844,21 @@ describe("Marketplace", function () {
|
||||
|
||||
it("refunds the client when slot is freed and not repaired", async function () {
|
||||
const payouts = await waitUntilStarted(marketplace, request, proof, token)
|
||||
|
||||
await expect(marketplace.freeSlot(slotId(slot))).to.emit(
|
||||
marketplace,
|
||||
"SlotFreed"
|
||||
)
|
||||
await advanceTime(10)
|
||||
await marketplace.freeSlot(slotId(slot))
|
||||
const freedAt = await currentTime()
|
||||
const requestEnd = await marketplace.requestEnd(requestId(request))
|
||||
await waitUntilFinished(marketplace, requestId(request))
|
||||
|
||||
switchAccount(client)
|
||||
const startBalance = await token.balanceOf(client.address)
|
||||
await marketplace.withdrawFunds(slot.request)
|
||||
const endBalance = await token.balanceOf(client.address)
|
||||
|
||||
const hostPayouts = payouts.reduce((a, b) => a + b, 0)
|
||||
const refund = payoutForDuration(request, freedAt, requestEnd)
|
||||
expect(endBalance - startBalance).to.equal(
|
||||
maxPrice(request) -
|
||||
payouts.reduce((a, b) => a + b, 0) + // This is the amount that user gets refunded for filling period in expiry window
|
||||
payouts[slot.index] // This is the refunded amount for the freed slot
|
||||
maxPrice(request) - hostPayouts + refund
|
||||
)
|
||||
})
|
||||
})
|
||||
@ -1195,13 +1195,14 @@ describe("Marketplace", function () {
|
||||
|
||||
switchAccount(validator)
|
||||
|
||||
const startBalance = await token.balanceOf(validator.address)
|
||||
|
||||
await waitUntilProofIsRequired(id)
|
||||
let missedPeriod = periodOf(await currentTime())
|
||||
await advanceTime(period + 1)
|
||||
await marketplace.markProofAsMissing(id, missedPeriod)
|
||||
|
||||
const startBalance = await token.balanceOf(validator.address)
|
||||
await waitUntilFinished(marketplace, slot.request)
|
||||
await marketplace.withdrawByValidator(slot.request)
|
||||
const endBalance = await token.balanceOf(validator.address)
|
||||
|
||||
const collateral = collateralPerSlot(request)
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user