[marketplace] Allow client to withdraw when cancelled

Adds ability for client to withdraw funds from a cancelled storage request.

Tests to check if request has timed out, if the client address is requesting withdraw, if the request state is new, and the funds were successfully transferred.
This commit is contained in:
Eric Mastro 2022-08-04 12:14:36 +10:00 committed by Eric Mastro
parent 1933ed489a
commit 37004e0e1f
3 changed files with 114 additions and 2 deletions

View File

@ -96,6 +96,25 @@ contract Marketplace is Collateral, Proofs {
require(token.transfer(slot.host, amount), "Payment failed");
}
function withdrawFunds(bytes32 requestId) public marketplaceInvariant {
Request memory request = requests[requestId];
require(block.timestamp > request.expiry, "Request not yet timed out");
require(request.client == msg.sender, "Invalid client address");
RequestContext storage context = requestContexts[requestId];
require(context.state == RequestState.New, "Invalid state");
uint256 amount = _price(request);
funds.sent += amount;
funds.balance -= amount;
token.transfer(msg.sender, amount);
emit FundsWithdrawn(requestId);
// Update request state to Cancelled. Handle in the withdraw transaction
// as there needs to be someone to pay for the gas to update the state
context.state = RequestState.Cancelled;
emit RequestCancelled(requestId);
}
function _host(bytes32 slotId) internal view returns (address) {
return slots[slotId].host;
}
@ -116,8 +135,20 @@ contract Marketplace is Collateral, Proofs {
return _end(slotId);
}
function _price(
uint64 numSlots,
uint256 duration,
uint256 reward) internal pure returns (uint256) {
return numSlots * duration * reward;
}
function _price(Request memory request) internal pure returns (uint256) {
return _price(request.ask.slots, request.ask.duration, request.ask.reward);
}
function price(Request calldata request) private pure returns (uint256) {
return request.ask.slots * request.ask.duration * request.ask.reward;
return _price(request.ask.slots, request.ask.duration, request.ask.reward);
}
function pricePerSlot(Request memory request) private pure returns (uint256) {
@ -164,7 +195,8 @@ contract Marketplace is Collateral, Proofs {
New, // [default] waiting to fill slots
Started, // all slots filled, accepting regular proofs
Cancelled, // not enough slots filled before expiry
Finished // successfully completed
Finished, // successfully completed
Failed // too many nodes have failed to provide proofs, data lost
}
struct RequestContext {
@ -184,6 +216,8 @@ contract Marketplace is Collateral, Proofs {
uint256 indexed slotIndex,
bytes32 indexed slotId
);
event RequestCancelled(bytes32 requestId);
event FundsWithdrawn(bytes32 requestId);
modifier marketplaceInvariant() {
MarketplaceFunds memory oldFunds = funds;

View File

@ -4,6 +4,7 @@ const { expect } = require("chai")
const { exampleRequest } = require("./examples")
const { now, hours } = require("./time")
const { requestId, slotId, askToArray } = require("./ids")
const { waitUntilExpired } = require("./marketplace")
const { price, pricePerSlot } = require("./price")
const {
snapshot,
@ -258,5 +259,77 @@ describe("Marketplace", function () {
}
await expect(await marketplace.state(slot.request)).to.equal(1)
})
it("fails when all slots are already filled", async function () {
const lastSlot = request.ask.slots - 1
for (let i = 0; i <= lastSlot; i++) {
await marketplace.fillSlot(slot.request, i, proof)
}
await expect(
marketplace.fillSlot(slot.request, lastSlot, proof)
).to.be.revertedWith("Invalid state")
})
})
describe("withdrawing funds", function () {
beforeEach(async function () {
switchAccount(client)
await token.approve(marketplace.address, price(request))
await marketplace.requestStorage(request)
switchAccount(host)
await token.approve(marketplace.address, collateral)
await marketplace.deposit(collateral)
})
it("rejects withdraw when request not yet timed out", async function () {
switchAccount(client)
await expect(marketplace.withdrawFunds(slot.request)).to.be.revertedWith(
"Request not yet timed out"
)
})
it("rejects withdraw when wrong account used", async function () {
await waitUntilExpired(request.expiry)
await expect(marketplace.withdrawFunds(slot.request)).to.be.revertedWith(
"Invalid client address"
)
})
it("rejects withdraw when in wrong state", async function () {
// fill all slots, should change state to RequestState.Started
const lastSlot = request.ask.slots - 1
for (let i = 0; i <= lastSlot; i++) {
await marketplace.fillSlot(slot.request, i, proof)
}
await waitUntilExpired(request.expiry)
switchAccount(client)
await expect(marketplace.withdrawFunds(slot.request)).to.be.revertedWith(
"Invalid state"
)
})
it("emits event once funds are withdrawn", async function () {
await waitUntilExpired(request.expiry)
switchAccount(client)
await expect(marketplace.withdrawFunds(slot.request))
.to.emit(marketplace, "FundsWithdrawn")
.withArgs(requestId(request))
})
it("emits event once request is cancelled", async function () {
await waitUntilExpired(request.expiry)
switchAccount(client)
await expect(marketplace.withdrawFunds(slot.request))
.to.emit(marketplace, "RequestCancelled")
.withArgs(requestId(request))
})
it("withdraws to the client", async function () {
await waitUntilExpired(request.expiry)
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(price(request))
})
})
})

5
test/marketplace.js Normal file
View File

@ -0,0 +1,5 @@
async function waitUntilExpired(expiry) {
await ethers.provider.send("hardhat_mine", [ethers.utils.hexValue(expiry)])
}
module.exports = { waitUntilExpired }