[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:
parent
1933ed489a
commit
37004e0e1f
|
@ -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;
|
||||
|
|
|
@ -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))
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
async function waitUntilExpired(expiry) {
|
||||
await ethers.provider.send("hardhat_mine", [ethers.utils.hexValue(expiry)])
|
||||
}
|
||||
|
||||
module.exports = { waitUntilExpired }
|
Loading…
Reference in New Issue