diff --git a/contracts/Marketplace.sol b/contracts/Marketplace.sol index 389a814..a255983 100644 --- a/contracts/Marketplace.sol +++ b/contracts/Marketplace.sol @@ -147,10 +147,14 @@ contract Marketplace is SlotReservations, Proofs, StateRetrieval, Endian { _addToMyRequests(request.client, id); + TokensPerSecond pricePerSecond = request.ask.pricePerSecond(); + uint128 price = pricePerSecond.accumulate(request.ask.duration); + FundId fund = id.asFundId(); AccountId account = _vault.clientAccount(request.client); _vault.lock(fund, expiresAt, endsAt); - _transferToVault(request.client, fund, account, request.maxPrice()); + _transferToVault(request.client, fund, account, price); + _vault.flow(fund, account, account, pricePerSecond); emit StorageRequested(id, request.ask, expiresAt); } @@ -195,25 +199,23 @@ contract Marketplace is SlotReservations, Proofs, StateRetrieval, Endian { context.slotsFilled += 1; - // Collect collateral - uint128 collateralAmount = request.collateralPerSlot(); - uint128 designatedAmount = _config.collateral.designatedCollateral( - collateralAmount - ); - if (slotState(slotId) == SlotState.Repair) { - // Host is repairing a slot and is entitled for repair reward, so he gets "discounted collateral" - // in this way he gets "physically" the reward at the end of the request when the full amount of collateral - // is returned to him. - collateralAmount -= _config.collateral.repairReward(collateralAmount); - } - FundId fund = requestId.asFundId(); AccountId clientAccount = _vault.clientAccount(request.client); AccountId hostAccount = _vault.hostAccount(slot.host, slotIndex); TokensPerSecond rate = request.ask.pricePerSlotPerSecond(); - _transferToVault(slot.host, fund, hostAccount, collateralAmount); - _vault.designate(fund, hostAccount, designatedAmount); + uint128 collateral = request.collateralPerSlot(); + uint128 designated = _config.collateral.designatedCollateral(collateral); + + if (slotState(slotId) == SlotState.Repair) { + // host gets a discount on its collateral, paid for by the repair reward + uint128 repairReward = _config.collateral.repairReward(collateral); + _vault.transfer(fund, clientAccount, hostAccount, repairReward); + collateral -= repairReward; + } + + _transferToVault(slot.host, fund, hostAccount, collateral); + _vault.designate(fund, hostAccount, designated); _vault.flow(fund, clientAccount, hostAccount, rate); _addToMySlots(slot.host, slotId); @@ -334,12 +336,15 @@ contract Marketplace is SlotReservations, Proofs, StateRetrieval, Endian { Request storage request = _requests[requestId]; + TokensPerSecond rate = request.ask.pricePerSlotPerSecond(); + uint128 collateral = request.collateralPerSlot(); + uint128 repairReward = _config.collateral.repairReward(collateral); + FundId fund = requestId.asFundId(); AccountId hostAccount = _vault.hostAccount(slot.host, slot.slotIndex); AccountId clientAccount = _vault.clientAccount(request.client); - TokensPerSecond rate = request.ask.pricePerSlotPerSecond(); - _vault.flow(fund, hostAccount, clientAccount, rate); + _vault.transfer(fund, hostAccount, clientAccount, repairReward); _vault.burnAccount(fund, hostAccount); _removeFromMySlots(slot.host, slotId); diff --git a/contracts/Requests.sol b/contracts/Requests.sol index d3f7e85..b347e9b 100644 --- a/contracts/Requests.sol +++ b/contracts/Requests.sol @@ -49,12 +49,19 @@ enum SlotState { } library AskHelpers { + using AskHelpers for Ask; + function pricePerSlotPerSecond( Ask memory ask ) internal pure returns (TokensPerSecond) { uint96 perByte = TokensPerSecond.unwrap(ask.pricePerBytePerSecond); return TokensPerSecond.wrap(perByte * ask.slotSize); } + + function pricePerSecond(Ask memory ask) internal pure returns (TokensPerSecond) { + uint96 perSlot = TokensPerSecond.unwrap(ask.pricePerSlotPerSecond()); + return TokensPerSecond.wrap(perSlot * ask.slots); + } } library Requests { @@ -89,11 +96,4 @@ library Requests { result := ids } } - - function maxPrice(Request memory request) internal pure returns (uint128) { - uint64 slots = request.ask.slots; - TokensPerSecond rate = request.ask.pricePerSlotPerSecond(); - Duration duration = request.ask.duration; - return slots * rate.accumulate(duration); - } } diff --git a/test/Marketplace.test.js b/test/Marketplace.test.js index 5268f23..bac2da2 100644 --- a/test/Marketplace.test.js +++ b/test/Marketplace.test.js @@ -28,7 +28,7 @@ const { pricePerSlotPerSecond, payoutForDuration, } = require("./price") -const { collateralPerSlot } = require("./collateral") +const { collateralPerSlot, repairReward } = require("./collateral") const { snapshot, revert, @@ -296,33 +296,39 @@ describe("Marketplace", function () { expect(await marketplace.getHost(slotId(slot))).to.equal(host.address) }) - it("gives discount on the collateral for repaired slot", async function () { - await marketplace.reserveSlot(slot.request, slot.index) - await marketplace.fillSlot(slot.request, slot.index, proof) - await marketplace.freeSlot(slotId(slot)) - expect(await marketplace.slotState(slotId(slot))).to.equal( - SlotState.Repair - ) + describe("when repairing a slot", function () { + beforeEach(async function () { + await marketplace.reserveSlot(slot.request, slot.index) + await marketplace.fillSlot(slot.request, slot.index, proof) + await advanceTime(config.proofs.period + 1) + await marketplace.freeSlot(slotId(slot)) + }) - // We need to advance the time to next period, because filling slot - // must not be done in the same period as for that period there was already proof - // submitted with the previous `fillSlot` and the transaction would revert with "Proof already submitted". - await advanceTime(config.proofs.period + 1) + it("gives the host a discount on the collateral", async function () { + const collateral = collateralPerSlot(request) + const reward = repairReward(config, collateral) + const discountedCollateral = collateral - reward + await token.approve(marketplace.address, discountedCollateral) - const startBalance = await token.balanceOf(host.address) - const collateral = collateralPerSlot(request) - const discountedCollateral = - collateral - - Math.round( - (collateral * config.collateral.repairRewardPercentage) / 100 - ) - await token.approve(marketplace.address, discountedCollateral) - await marketplace.fillSlot(slot.request, slot.index, proof) - const endBalance = await token.balanceOf(host.address) - expect(startBalance - endBalance).to.equal(discountedCollateral) - expect(await marketplace.slotState(slotId(slot))).to.equal( - SlotState.Filled - ) + const startBalance = await token.balanceOf(host.address) + await marketplace.fillSlot(slot.request, slot.index, proof) + const endBalance = await token.balanceOf(host.address) + + expect(startBalance - endBalance).to.equal(discountedCollateral) + }) + + it("tops up the host collateral with the repair reward", async function () { + const collateral = collateralPerSlot(request) + const reward = repairReward(config, collateral) + const discountedCollateral = collateral - reward + await token.approve(marketplace.address, discountedCollateral) + + const startBalance = await marketplace.getSlotBalance(slotId(slot)) + await marketplace.fillSlot(slot.request, slot.index, proof) + const endBalance = await marketplace.getSlotBalance(slotId(slot)) + + expect(endBalance - startBalance).to.equal(collateral) + }) }) it("fails to retrieve a request of an empty slot", async function () { @@ -850,8 +856,9 @@ describe("Marketplace", function () { const hostPayouts = payouts.reduce((a, b) => a + b, 0) const refund = payoutForDuration(request, freedAt, requestEnd) + const reward = repairReward(config, collateralPerSlot(request)) expect(endBalance - startBalance).to.equal( - maxPrice(request) - hostPayouts + refund + maxPrice(request) - hostPayouts + refund + reward ) }) }) diff --git a/test/collateral.js b/test/collateral.js index ce60bf1..99b0500 100644 --- a/test/collateral.js +++ b/test/collateral.js @@ -2,4 +2,9 @@ function collateralPerSlot(request) { return request.ask.collateralPerByte * request.ask.slotSize } -module.exports = { collateralPerSlot } +function repairReward(configuration, collateral) { + const percentage = configuration.collateral.repairRewardPercentage + return Math.round((collateral * percentage) / 100) +} + +module.exports = { collateralPerSlot, repairReward }