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 "./StateRetrieval.sol";
import "./Endian.sol"; import "./Endian.sol";
import "./Groth16.sol"; import "./Groth16.sol";
import "./marketplace/VaultHelpers.sol";
contract Marketplace is SlotReservations, Proofs, StateRetrieval, Endian { contract Marketplace is SlotReservations, Proofs, StateRetrieval, Endian {
error Marketplace_RepairRewardPercentageTooHigh(); error Marketplace_RepairRewardPercentageTooHigh();
@ -46,6 +47,9 @@ contract Marketplace is SlotReservations, Proofs, StateRetrieval, Endian {
using EnumerableSet for EnumerableSet.AddressSet; using EnumerableSet for EnumerableSet.AddressSet;
using Requests for Request; using Requests for Request;
using AskHelpers for Ask; using AskHelpers for Ask;
using VaultHelpers for Vault;
using VaultHelpers for RequestId;
using VaultHelpers for Request;
Vault private immutable _vault; Vault private immutable _vault;
MarketplaceConfig private _config; MarketplaceConfig private _config;
@ -173,10 +177,18 @@ contract Marketplace is SlotReservations, Proofs, StateRetrieval, Endian {
_addToMyRequests(request.client, id); _addToMyRequests(request.client, id);
uint256 amount = request.maxPrice(); uint128 amount = uint128(request.maxPrice());
_requestContexts[id].fundsToReturnToClient = amount; _requestContexts[id].fundsToReturnToClient = amount;
_marketplaceTotals.received += 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); emit StorageRequested(id, request.ask, _requestContexts[id].expiresAt);
} }
@ -236,7 +248,14 @@ contract Marketplace is SlotReservations, Proofs, StateRetrieval, Endian {
} else { } else {
collateralAmount = collateralPerSlot; 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; _marketplaceTotals.received += collateralAmount;
slot.currentCollateral = collateralPerSlot; // Even if he has collateral discounted, he is operating with full collateral 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.state = RequestState.Started;
context.startedAt = uint64(block.timestamp); context.startedAt = uint64(block.timestamp);
_vault.extendLock(fund, Timestamp.wrap(uint40(context.endsAt)));
emit RequestFulfilled(requestId); 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 * @notice Frees a slot, paying out rewards and returning collateral for
finished or cancelled requests to the host that has filled the slot. 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 * uint256 validatorRewardAmount = (slashedAmount *
_config.collateral.validatorRewardPercentage) / 100; _config.collateral.validatorRewardPercentage) / 100;
_marketplaceTotals.sent += validatorRewardAmount; _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; slot.currentCollateral -= slashedAmount;
if (missingProofs(slotId) >= _config.collateral.maxNumberOfSlashes) { 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. // we keep correctly the track of the funds that needs to be returned at the end.
context.fundsToReturnToClient += _slotPayout(requestId, slot.filledAt); 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); _removeFromMySlots(slot.host, slotId);
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;
@ -364,14 +418,14 @@ contract Marketplace is SlotReservations, Proofs, StateRetrieval, Endian {
emit SlotFreed(requestId, slot.slotIndex); emit SlotFreed(requestId, slot.slotIndex);
_resetMissingProofs(slotId); _resetMissingProofs(slotId);
Request storage request = _requests[requestId];
uint256 slotsLost = request.ask.slots - context.slotsFilled; uint256 slotsLost = request.ask.slots - context.slotsFilled;
if ( if (
slotsLost > request.ask.maxSlotLoss && slotsLost > request.ask.maxSlotLoss &&
context.state == RequestState.Started context.state == RequestState.Started
) { ) {
context.state = RequestState.Failed; context.state = RequestState.Failed;
context.endsAt = uint64(block.timestamp) - 1; _vault.freezeFund(fund);
emit RequestFailed(requestId); emit RequestFailed(requestId);
} }
} }
@ -392,7 +446,9 @@ contract Marketplace is SlotReservations, Proofs, StateRetrieval, Endian {
uint256 collateralAmount = slot.currentCollateral; uint256 collateralAmount = slot.currentCollateral;
_marketplaceTotals.sent += (payoutAmount + collateralAmount); _marketplaceTotals.sent += (payoutAmount + collateralAmount);
slot.state = SlotState.Paid; 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; uint256 collateralAmount = slot.currentCollateral;
_marketplaceTotals.sent += (payoutAmount + collateralAmount); _marketplaceTotals.sent += (payoutAmount + collateralAmount);
slot.state = SlotState.Paid; 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; uint256 amount = context.fundsToReturnToClient;
_marketplaceTotals.sent += amount; _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 // We zero out the funds tracking in order to prevent double-spends
context.fundsToReturnToClient = 0; context.fundsToReturnToClient = 0;
} }
function withdrawByValidator(RequestId requestId) public {
FundId fund = requestId.asFundId();
AccountId account = _vault.validatorAccount(msg.sender);
_vault.withdraw(fund, account);
}
function getActiveSlot( function getActiveSlot(
SlotId slotId SlotId slotId
) public view slotIsNotFree(slotId) returns (ActiveSlot memory) { ) 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) { function requestEnd(RequestId requestId) public view returns (uint64) {
RequestState state = requestState(requestId); 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; return _requestContexts[requestId].endsAt;
} }
if (state == RequestState.Cancelled) { 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 "./Accounts.sol";
import "./Funds.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. /// Records account balances and token flows. Accounts are separated into funds.
/// Funds are kept separate between controllers. /// 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 /// Represents a smart contract that can redistribute and burn tokens in funds
type Controller is address; 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 /// Each controller has its own set of funds
mapping(Controller => mapping(FundId => Fund)) private _funds; mapping(Controller => mapping(FundId => Fund)) private _funds;

View File

@ -23,7 +23,11 @@ const {
waitUntilSlotFailed, waitUntilSlotFailed,
patchOverloads, patchOverloads,
} = require("./marketplace") } = require("./marketplace")
const { maxPrice, pricePerSlotPerSecond } = require("./price") const {
maxPrice,
pricePerSlotPerSecond,
payoutForDuration,
} = require("./price")
const { collateralPerSlot } = require("./collateral") const { collateralPerSlot } = require("./collateral")
const { const {
snapshot, snapshot,
@ -500,15 +504,6 @@ describe("Marketplace", function () {
).to.equal(requestTime + request.ask.duration) ).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 () { it("sets request end time to the past once cancelled", async function () {
await marketplace.reserveSlot(slot.request, slot.index) await marketplace.reserveSlot(slot.request, slot.index)
await marketplace.fillSlot(slot.request, slot.index, proof) await marketplace.fillSlot(slot.request, slot.index, proof)
@ -583,7 +578,7 @@ describe("Marketplace", function () {
await token.approve(marketplace.address, collateral) 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 // 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 // in the "expiry window" and not at its beginning. This is more "real" setup
// and demonstrates the partial payout feature better. // and demonstrates the partial payout feature better.
@ -601,7 +596,6 @@ describe("Marketplace", function () {
await marketplace.freeSlot(slotId(slot)) await marketplace.freeSlot(slotId(slot))
const endBalanceHost = await token.balanceOf(host.address) const endBalanceHost = await token.balanceOf(host.address)
expect(expectedPayouts[slot.index]).to.be.lt(maxPrice(request))
const collateral = collateralPerSlot(request) const collateral = collateralPerSlot(request)
expect(endBalanceHost - startBalanceHost).to.equal( expect(endBalanceHost - startBalanceHost).to.equal(
expectedPayouts[slot.index] + collateral expectedPayouts[slot.index] + collateral
@ -805,18 +799,24 @@ describe("Marketplace", function () {
expect(endBalance - startBalance).to.equal(maxPrice(request)) 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 waitUntilStarted(marketplace, request, proof, token)
await waitUntilFailed(marketplace, request) await waitUntilFailed(marketplace, request)
const failedAt = await currentTime()
await waitUntilFinished(marketplace, requestId(request))
const finishedAt = await currentTime()
switchAccount(client) switchAccount(client)
const startBalance = await token.balanceOf(client.address) const startBalance = await token.balanceOf(client.address)
await marketplace.withdrawFunds(slot.request) await marketplace.withdrawFunds(slot.request)
const endBalance = await token.balanceOf(client.address) 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 () { 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 () { it("refunds the client when slot is freed and not repaired", async function () {
const payouts = await waitUntilStarted(marketplace, request, proof, token) const payouts = await waitUntilStarted(marketplace, request, proof, token)
await advanceTime(10)
await expect(marketplace.freeSlot(slotId(slot))).to.emit( await marketplace.freeSlot(slotId(slot))
marketplace, const freedAt = await currentTime()
"SlotFreed" const requestEnd = await marketplace.requestEnd(requestId(request))
)
await waitUntilFinished(marketplace, requestId(request)) await waitUntilFinished(marketplace, requestId(request))
switchAccount(client) switchAccount(client)
const startBalance = await token.balanceOf(client.address) const startBalance = await token.balanceOf(client.address)
await marketplace.withdrawFunds(slot.request) await marketplace.withdrawFunds(slot.request)
const endBalance = await token.balanceOf(client.address) 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( expect(endBalance - startBalance).to.equal(
maxPrice(request) - maxPrice(request) - hostPayouts + refund
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
) )
}) })
}) })
@ -1195,13 +1195,14 @@ describe("Marketplace", function () {
switchAccount(validator) switchAccount(validator)
const startBalance = await token.balanceOf(validator.address)
await waitUntilProofIsRequired(id) await waitUntilProofIsRequired(id)
let missedPeriod = periodOf(await currentTime()) let missedPeriod = periodOf(await currentTime())
await advanceTime(period + 1) await advanceTime(period + 1)
await marketplace.markProofAsMissing(id, missedPeriod) 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 endBalance = await token.balanceOf(validator.address)
const collateral = collateralPerSlot(request) const collateral = collateralPerSlot(request)