marketplace: use vault in marketplace

This commit is contained in:
Mark Spanbroek 2025-02-25 15:53:01 +01:00
parent 8b40f63693
commit 8df557801c
4 changed files with 162 additions and 37 deletions

View File

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

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

View File

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

View File

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