feat: adds an optional `payoutAddress` to allow payouts to be paid to separate address (#144)
* initial commit for splitting payouts Collateral goes to slot's host address, while reward payouts go to the slot's host payoutAddress * Add fillSlot overload to make payoutAddress "optional" * add tests for payoutAddress * add doc to patchFillSlotOverloads * formatting * remove optional payoutAddress parameter * Move payoutAddress to freeSlot - remove payoutAddress parameter from `fillSlot` - remove `payoutAddress` from slot struct and storage - add payoutAddress parameter to `freeSlot`, preventing the need for storage * formatting * update certora spec to match updated function signature * Add withdrawAddress to withdrawFunds - prevent erc20 msg.sender blacklisting * Update tests for paying out to withdrawAddress * formatting * Add collateralRecipient * refactor: change withdrawFunds and freeSlot overloads - `withdrawFunds` now has an option withdrawRecipient parameter - `freeSlot` now has two optional parameters: rewardRecipient, and collateralRecipient. Both or none must be specified. * update certora spec for new sigs
This commit is contained in:
parent
29f39d52c7
commit
73a2ca0bd3
|
@ -111,12 +111,12 @@ function canStartRequest(method f) returns bool {
|
||||||
}
|
}
|
||||||
|
|
||||||
function canFinishRequest(method f) returns bool {
|
function canFinishRequest(method f) returns bool {
|
||||||
return f.selector == sig:freeSlot(Marketplace.SlotId).selector;
|
return f.selector == sig:freeSlot(Marketplace.SlotId, address, address).selector;
|
||||||
}
|
}
|
||||||
|
|
||||||
function canFailRequest(method f) returns bool {
|
function canFailRequest(method f) returns bool {
|
||||||
return f.selector == sig:markProofAsMissing(Marketplace.SlotId, Periods.Period).selector ||
|
return f.selector == sig:markProofAsMissing(Marketplace.SlotId, Periods.Period).selector ||
|
||||||
f.selector == sig:freeSlot(Marketplace.SlotId).selector;
|
f.selector == sig:freeSlot(Marketplace.SlotId, address, address).selector;
|
||||||
}
|
}
|
||||||
|
|
||||||
/*--------------------------------------------
|
/*--------------------------------------------
|
||||||
|
|
|
@ -46,7 +46,7 @@ contract Marketplace is Proofs, StateRetrieval, Endian {
|
||||||
/// @dev When Slot is filled, the collateral is collected in amount of request.ask.collateral
|
/// @dev When Slot is filled, the collateral is collected in amount of request.ask.collateral
|
||||||
/// @dev When Host is slashed for missing a proof the slashed amount is reflected in this variable
|
/// @dev When Host is slashed for missing a proof the slashed amount is reflected in this variable
|
||||||
uint256 currentCollateral;
|
uint256 currentCollateral;
|
||||||
address host;
|
address host; // address used for collateral interactions and identifying hosts
|
||||||
}
|
}
|
||||||
|
|
||||||
struct ActiveSlot {
|
struct ActiveSlot {
|
||||||
|
@ -114,6 +114,14 @@ contract Marketplace is Proofs, StateRetrieval, Endian {
|
||||||
emit StorageRequested(id, request.ask, _requestContexts[id].expiresAt);
|
emit StorageRequested(id, request.ask, _requestContexts[id].expiresAt);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @notice Fills a slot. Reverts if an invalid proof of the slot data is
|
||||||
|
provided.
|
||||||
|
* @param requestId RequestId identifying the request containing the slot to
|
||||||
|
fill.
|
||||||
|
* @param slotIndex Index of the slot in the request.
|
||||||
|
* @param proof Groth16 proof procing possession of the slot data.
|
||||||
|
*/
|
||||||
function fillSlot(
|
function fillSlot(
|
||||||
RequestId requestId,
|
RequestId requestId,
|
||||||
uint256 slotIndex,
|
uint256 slotIndex,
|
||||||
|
@ -158,19 +166,48 @@ contract Marketplace is Proofs, StateRetrieval, Endian {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @notice Frees a slot, paying out rewards and returning collateral for
|
||||||
|
finished or cancelled requests to the host that has filled the slot.
|
||||||
|
* @param slotId id of the slot to free
|
||||||
|
* @dev The host that filled the slot must have initiated the transaction
|
||||||
|
(msg.sender). This overload allows `rewardRecipient` and
|
||||||
|
`collateralRecipient` to be optional.
|
||||||
|
*/
|
||||||
function freeSlot(SlotId slotId) public slotIsNotFree(slotId) {
|
function freeSlot(SlotId slotId) public slotIsNotFree(slotId) {
|
||||||
|
return freeSlot(slotId, msg.sender, msg.sender);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @notice Frees a slot, paying out rewards and returning collateral for
|
||||||
|
finished or cancelled requests.
|
||||||
|
* @param slotId id of the slot to free
|
||||||
|
* @param rewardRecipient address to send rewards to
|
||||||
|
* @param collateralRecipient address to refund collateral to
|
||||||
|
*/
|
||||||
|
function freeSlot(
|
||||||
|
SlotId slotId,
|
||||||
|
address rewardRecipient,
|
||||||
|
address collateralRecipient
|
||||||
|
) public slotIsNotFree(slotId) {
|
||||||
Slot storage slot = _slots[slotId];
|
Slot storage slot = _slots[slotId];
|
||||||
require(slot.host == msg.sender, "Slot filled by other host");
|
require(slot.host == msg.sender, "Slot filled by other host");
|
||||||
SlotState state = slotState(slotId);
|
SlotState state = slotState(slotId);
|
||||||
require(state != SlotState.Paid, "Already paid");
|
require(state != SlotState.Paid, "Already paid");
|
||||||
|
|
||||||
if (state == SlotState.Finished) {
|
if (state == SlotState.Finished) {
|
||||||
_payoutSlot(slot.requestId, slotId);
|
_payoutSlot(slot.requestId, slotId, rewardRecipient, collateralRecipient);
|
||||||
} else if (state == SlotState.Cancelled) {
|
} else if (state == SlotState.Cancelled) {
|
||||||
_payoutCancelledSlot(slot.requestId, slotId);
|
_payoutCancelledSlot(
|
||||||
|
slot.requestId,
|
||||||
|
slotId,
|
||||||
|
rewardRecipient,
|
||||||
|
collateralRecipient
|
||||||
|
);
|
||||||
} else if (state == SlotState.Failed) {
|
} else if (state == SlotState.Failed) {
|
||||||
_removeFromMySlots(msg.sender, slotId);
|
_removeFromMySlots(msg.sender, slotId);
|
||||||
} else if (state == SlotState.Filled) {
|
} else if (state == SlotState.Filled) {
|
||||||
|
// free slot without returning collateral, effectively a 100% slash
|
||||||
_forciblyFreeSlot(slotId);
|
_forciblyFreeSlot(slotId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -231,6 +268,13 @@ contract Marketplace is Proofs, StateRetrieval, Endian {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @notice Abandons the slot without returning collateral, effectively slashing the
|
||||||
|
entire collateral.
|
||||||
|
* @param slotId SlotId of the slot to free.
|
||||||
|
* @dev _slots[slotId] is deleted, resetting _slots[slotId].currentCollateral
|
||||||
|
to 0.
|
||||||
|
*/
|
||||||
function _forciblyFreeSlot(SlotId slotId) internal {
|
function _forciblyFreeSlot(SlotId slotId) internal {
|
||||||
Slot storage slot = _slots[slotId];
|
Slot storage slot = _slots[slotId];
|
||||||
RequestId requestId = slot.requestId;
|
RequestId requestId = slot.requestId;
|
||||||
|
@ -260,7 +304,9 @@ contract Marketplace is Proofs, StateRetrieval, Endian {
|
||||||
|
|
||||||
function _payoutSlot(
|
function _payoutSlot(
|
||||||
RequestId requestId,
|
RequestId requestId,
|
||||||
SlotId slotId
|
SlotId slotId,
|
||||||
|
address rewardRecipient,
|
||||||
|
address collateralRecipient
|
||||||
) private requestIsKnown(requestId) {
|
) private requestIsKnown(requestId) {
|
||||||
RequestContext storage context = _requestContexts[requestId];
|
RequestContext storage context = _requestContexts[requestId];
|
||||||
Request storage request = _requests[requestId];
|
Request storage request = _requests[requestId];
|
||||||
|
@ -270,31 +316,62 @@ contract Marketplace is Proofs, StateRetrieval, Endian {
|
||||||
|
|
||||||
_removeFromMySlots(slot.host, slotId);
|
_removeFromMySlots(slot.host, slotId);
|
||||||
|
|
||||||
uint256 amount = _requests[requestId].pricePerSlot() +
|
uint256 payoutAmount = _requests[requestId].pricePerSlot();
|
||||||
slot.currentCollateral;
|
uint256 collateralAmount = slot.currentCollateral;
|
||||||
_marketplaceTotals.sent += amount;
|
_marketplaceTotals.sent += (payoutAmount + collateralAmount);
|
||||||
slot.state = SlotState.Paid;
|
slot.state = SlotState.Paid;
|
||||||
assert(_token.transfer(slot.host, amount));
|
assert(_token.transfer(rewardRecipient, payoutAmount));
|
||||||
|
assert(_token.transfer(collateralRecipient, collateralAmount));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @notice Pays out a host for duration of time that the slot was filled, and
|
||||||
|
returns the collateral.
|
||||||
|
* @dev The payouts are sent to the rewardRecipient, and collateral is returned
|
||||||
|
to the host address.
|
||||||
|
* @param requestId RequestId of the request that contains the slot to be paid
|
||||||
|
out.
|
||||||
|
* @param slotId SlotId of the slot to be paid out.
|
||||||
|
*/
|
||||||
function _payoutCancelledSlot(
|
function _payoutCancelledSlot(
|
||||||
RequestId requestId,
|
RequestId requestId,
|
||||||
SlotId slotId
|
SlotId slotId,
|
||||||
|
address rewardRecipient,
|
||||||
|
address collateralRecipient
|
||||||
) private requestIsKnown(requestId) {
|
) private requestIsKnown(requestId) {
|
||||||
Slot storage slot = _slots[slotId];
|
Slot storage slot = _slots[slotId];
|
||||||
_removeFromMySlots(slot.host, slotId);
|
_removeFromMySlots(slot.host, slotId);
|
||||||
|
|
||||||
uint256 amount = _expiryPayoutAmount(requestId, slot.filledAt) +
|
uint256 payoutAmount = _expiryPayoutAmount(requestId, slot.filledAt);
|
||||||
slot.currentCollateral;
|
uint256 collateralAmount = slot.currentCollateral;
|
||||||
_marketplaceTotals.sent += amount;
|
_marketplaceTotals.sent += (payoutAmount + collateralAmount);
|
||||||
slot.state = SlotState.Paid;
|
slot.state = SlotState.Paid;
|
||||||
assert(_token.transfer(slot.host, amount));
|
assert(_token.transfer(rewardRecipient, payoutAmount));
|
||||||
|
assert(_token.transfer(collateralRecipient, collateralAmount));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// @notice Withdraws storage request funds back to the client that deposited them.
|
/**
|
||||||
/// @dev Request must be expired, must be in RequestState.New, and the transaction must originate from the depositer address.
|
* @notice Withdraws remaining storage request funds back to the client that
|
||||||
/// @param requestId the id of the request
|
deposited them.
|
||||||
|
* @dev Request must be expired, must be in RequestStat e.New, and the
|
||||||
|
transaction must originate from the depositer address.
|
||||||
|
* @param requestId the id of the request
|
||||||
|
*/
|
||||||
function withdrawFunds(RequestId requestId) public {
|
function withdrawFunds(RequestId requestId) public {
|
||||||
|
withdrawFunds(requestId, msg.sender);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @notice Withdraws storage request funds to the provided address.
|
||||||
|
* @dev Request must be expired, must be in RequestState.New, and the
|
||||||
|
transaction must originate from the depositer address.
|
||||||
|
* @param requestId the id of the request
|
||||||
|
* @param withdrawRecipient address to return the remaining funds to
|
||||||
|
*/
|
||||||
|
function withdrawFunds(
|
||||||
|
RequestId requestId,
|
||||||
|
address withdrawRecipient
|
||||||
|
) public {
|
||||||
Request storage request = _requests[requestId];
|
Request storage request = _requests[requestId];
|
||||||
require(
|
require(
|
||||||
block.timestamp > requestExpiry(requestId),
|
block.timestamp > requestExpiry(requestId),
|
||||||
|
@ -313,7 +390,7 @@ contract Marketplace is Proofs, StateRetrieval, Endian {
|
||||||
|
|
||||||
uint256 amount = context.expiryFundsWithdraw;
|
uint256 amount = context.expiryFundsWithdraw;
|
||||||
_marketplaceTotals.sent += amount;
|
_marketplaceTotals.sent += amount;
|
||||||
assert(_token.transfer(msg.sender, amount));
|
assert(_token.transfer(withdrawRecipient, amount));
|
||||||
}
|
}
|
||||||
|
|
||||||
function getActiveSlot(
|
function getActiveSlot(
|
||||||
|
@ -356,7 +433,14 @@ contract Marketplace is Proofs, StateRetrieval, Endian {
|
||||||
return _requestContexts[requestId].expiresAt;
|
return _requestContexts[requestId].expiresAt;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// @notice Calculates the amount that should be payed out to a host if a request expires based on when the host fills the slot
|
/**
|
||||||
|
* @notice Calculates the amount that should be paid out to a host if a request
|
||||||
|
* expires based on when the host fills the slot
|
||||||
|
* @param requestId RequestId of the request used to calculate the payout
|
||||||
|
* amount.
|
||||||
|
* @param startingTimestamp timestamp indicating when a host filled a slot and
|
||||||
|
* started providing proofs.
|
||||||
|
*/
|
||||||
function _expiryPayoutAmount(
|
function _expiryPayoutAmount(
|
||||||
RequestId requestId,
|
RequestId requestId,
|
||||||
uint256 startingTimestamp
|
uint256 startingTimestamp
|
||||||
|
|
|
@ -21,6 +21,7 @@ const {
|
||||||
waitUntilFinished,
|
waitUntilFinished,
|
||||||
waitUntilFailed,
|
waitUntilFailed,
|
||||||
waitUntilSlotFailed,
|
waitUntilSlotFailed,
|
||||||
|
patchOverloads,
|
||||||
} = require("./marketplace")
|
} = require("./marketplace")
|
||||||
const { price, pricePerSlot } = require("./price")
|
const { price, pricePerSlot } = require("./price")
|
||||||
const {
|
const {
|
||||||
|
@ -87,7 +88,14 @@ describe("Marketplace", function () {
|
||||||
let marketplace
|
let marketplace
|
||||||
let token
|
let token
|
||||||
let verifier
|
let verifier
|
||||||
let client, host, host1, host2, host3
|
let client,
|
||||||
|
clientWithdrawRecipient,
|
||||||
|
host,
|
||||||
|
host1,
|
||||||
|
host2,
|
||||||
|
host3,
|
||||||
|
hostRewardRecipient,
|
||||||
|
hostCollateralRecipient
|
||||||
let request
|
let request
|
||||||
let slot
|
let slot
|
||||||
|
|
||||||
|
@ -96,12 +104,28 @@ describe("Marketplace", function () {
|
||||||
beforeEach(async function () {
|
beforeEach(async function () {
|
||||||
await snapshot()
|
await snapshot()
|
||||||
await ensureMinimumBlockHeight(256)
|
await ensureMinimumBlockHeight(256)
|
||||||
;[client, host1, host2, host3] = await ethers.getSigners()
|
;[
|
||||||
|
client,
|
||||||
|
clientWithdrawRecipient,
|
||||||
|
host1,
|
||||||
|
host2,
|
||||||
|
host3,
|
||||||
|
hostRewardRecipient,
|
||||||
|
hostCollateralRecipient,
|
||||||
|
] = await ethers.getSigners()
|
||||||
host = host1
|
host = host1
|
||||||
|
|
||||||
const TestToken = await ethers.getContractFactory("TestToken")
|
const TestToken = await ethers.getContractFactory("TestToken")
|
||||||
token = await TestToken.deploy()
|
token = await TestToken.deploy()
|
||||||
for (let account of [client, host1, host2, host3]) {
|
for (let account of [
|
||||||
|
client,
|
||||||
|
clientWithdrawRecipient,
|
||||||
|
host1,
|
||||||
|
host2,
|
||||||
|
host3,
|
||||||
|
hostRewardRecipient,
|
||||||
|
hostCollateralRecipient,
|
||||||
|
]) {
|
||||||
await token.mint(account.address, ACCOUNT_STARTING_BALANCE)
|
await token.mint(account.address, ACCOUNT_STARTING_BALANCE)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -114,6 +138,7 @@ describe("Marketplace", function () {
|
||||||
token.address,
|
token.address,
|
||||||
verifier.address
|
verifier.address
|
||||||
)
|
)
|
||||||
|
patchOverloads(marketplace)
|
||||||
|
|
||||||
request = await exampleRequest()
|
request = await exampleRequest()
|
||||||
request.client = client.address
|
request.client = client.address
|
||||||
|
@ -131,6 +156,7 @@ describe("Marketplace", function () {
|
||||||
function switchAccount(account) {
|
function switchAccount(account) {
|
||||||
token = token.connect(account)
|
token = token.connect(account)
|
||||||
marketplace = marketplace.connect(account)
|
marketplace = marketplace.connect(account)
|
||||||
|
patchOverloads(marketplace)
|
||||||
}
|
}
|
||||||
|
|
||||||
describe("requesting storage", function () {
|
describe("requesting storage", function () {
|
||||||
|
@ -481,6 +507,37 @@ describe("Marketplace", function () {
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it("pays to host reward address when contract has finished and returns collateral to host collateral address", async function () {
|
||||||
|
await waitUntilStarted(marketplace, request, proof, token)
|
||||||
|
await waitUntilFinished(marketplace, requestId(request))
|
||||||
|
const startBalanceHost = await token.balanceOf(host.address)
|
||||||
|
const startBalanceReward = await token.balanceOf(
|
||||||
|
hostRewardRecipient.address
|
||||||
|
)
|
||||||
|
const startBalanceCollateral = await token.balanceOf(
|
||||||
|
hostCollateralRecipient.address
|
||||||
|
)
|
||||||
|
await marketplace.freeSlot(
|
||||||
|
slotId(slot),
|
||||||
|
hostRewardRecipient.address,
|
||||||
|
hostCollateralRecipient.address
|
||||||
|
)
|
||||||
|
const endBalanceHost = await token.balanceOf(host.address)
|
||||||
|
const endBalanceReward = await token.balanceOf(
|
||||||
|
hostRewardRecipient.address
|
||||||
|
)
|
||||||
|
const endBalanceCollateral = await token.balanceOf(
|
||||||
|
hostCollateralRecipient.address
|
||||||
|
)
|
||||||
|
expect(endBalanceHost).to.equal(startBalanceHost)
|
||||||
|
expect(endBalanceCollateral - startBalanceCollateral).to.equal(
|
||||||
|
request.ask.collateral
|
||||||
|
)
|
||||||
|
expect(endBalanceReward - startBalanceReward).to.equal(
|
||||||
|
pricePerSlot(request)
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
it("pays the host when contract was cancelled", async function () {
|
it("pays the host when contract was cancelled", async function () {
|
||||||
// Lets advance the time more into the expiry window
|
// Lets advance the time more into the expiry window
|
||||||
const filledAt = (await currentTime()) + Math.floor(request.expiry / 3)
|
const filledAt = (await currentTime()) + Math.floor(request.expiry / 3)
|
||||||
|
@ -500,12 +557,69 @@ describe("Marketplace", function () {
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it("pays to host reward address when contract was cancelled, and returns collateral to host address", async function () {
|
||||||
|
// Lets advance the time more into the expiry window
|
||||||
|
const filledAt = (await currentTime()) + Math.floor(request.expiry / 3)
|
||||||
|
const expiresAt = (
|
||||||
|
await marketplace.requestExpiry(requestId(request))
|
||||||
|
).toNumber()
|
||||||
|
|
||||||
|
await advanceTimeToForNextBlock(filledAt)
|
||||||
|
await marketplace.fillSlot(slot.request, slot.index, proof)
|
||||||
|
await waitUntilCancelled(request)
|
||||||
|
const startBalanceHost = await token.balanceOf(host.address)
|
||||||
|
const startBalanceReward = await token.balanceOf(
|
||||||
|
hostRewardRecipient.address
|
||||||
|
)
|
||||||
|
const startBalanceCollateral = await token.balanceOf(
|
||||||
|
hostCollateralRecipient.address
|
||||||
|
)
|
||||||
|
await marketplace.freeSlot(
|
||||||
|
slotId(slot),
|
||||||
|
hostRewardRecipient.address,
|
||||||
|
hostCollateralRecipient.address
|
||||||
|
)
|
||||||
|
|
||||||
|
const expectedPartialPayout = (expiresAt - filledAt) * request.ask.reward
|
||||||
|
|
||||||
|
const endBalanceReward = await token.balanceOf(
|
||||||
|
hostRewardRecipient.address
|
||||||
|
)
|
||||||
|
expect(endBalanceReward - startBalanceReward).to.be.equal(
|
||||||
|
expectedPartialPayout
|
||||||
|
)
|
||||||
|
|
||||||
|
const endBalanceHost = await token.balanceOf(host.address)
|
||||||
|
expect(endBalanceHost).to.be.equal(startBalanceHost)
|
||||||
|
|
||||||
|
const endBalanceCollateral = await token.balanceOf(
|
||||||
|
hostCollateralRecipient.address
|
||||||
|
)
|
||||||
|
expect(endBalanceCollateral - startBalanceCollateral).to.be.equal(
|
||||||
|
request.ask.collateral
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
it("does not pay when the contract hasn't ended", async function () {
|
it("does not pay when the contract hasn't ended", async function () {
|
||||||
await marketplace.fillSlot(slot.request, slot.index, proof)
|
await marketplace.fillSlot(slot.request, slot.index, proof)
|
||||||
const startBalance = await token.balanceOf(host.address)
|
const startBalanceHost = await token.balanceOf(host.address)
|
||||||
|
const startBalanceReward = await token.balanceOf(
|
||||||
|
hostRewardRecipient.address
|
||||||
|
)
|
||||||
|
const startBalanceCollateral = await token.balanceOf(
|
||||||
|
hostCollateralRecipient.address
|
||||||
|
)
|
||||||
await marketplace.freeSlot(slotId(slot))
|
await marketplace.freeSlot(slotId(slot))
|
||||||
const endBalance = await token.balanceOf(host.address)
|
const endBalanceHost = await token.balanceOf(host.address)
|
||||||
expect(endBalance).to.equal(startBalance)
|
const endBalanceReward = await token.balanceOf(
|
||||||
|
hostRewardRecipient.address
|
||||||
|
)
|
||||||
|
const endBalanceCollateral = await token.balanceOf(
|
||||||
|
hostCollateralRecipient.address
|
||||||
|
)
|
||||||
|
expect(endBalanceHost).to.equal(startBalanceHost)
|
||||||
|
expect(endBalanceReward).to.equal(startBalanceReward)
|
||||||
|
expect(endBalanceCollateral).to.equal(startBalanceCollateral)
|
||||||
})
|
})
|
||||||
|
|
||||||
it("can only be done once", async function () {
|
it("can only be done once", async function () {
|
||||||
|
@ -586,16 +700,16 @@ describe("Marketplace", function () {
|
||||||
|
|
||||||
it("rejects withdraw when request not yet timed out", async function () {
|
it("rejects withdraw when request not yet timed out", async function () {
|
||||||
switchAccount(client)
|
switchAccount(client)
|
||||||
await expect(marketplace.withdrawFunds(slot.request)).to.be.revertedWith(
|
await expect(
|
||||||
"Request not yet timed out"
|
marketplace.withdrawFunds(slot.request, clientWithdrawRecipient.address)
|
||||||
)
|
).to.be.revertedWith("Request not yet timed out")
|
||||||
})
|
})
|
||||||
|
|
||||||
it("rejects withdraw when wrong account used", async function () {
|
it("rejects withdraw when wrong account used", async function () {
|
||||||
await waitUntilCancelled(request)
|
await waitUntilCancelled(request)
|
||||||
await expect(marketplace.withdrawFunds(slot.request)).to.be.revertedWith(
|
await expect(
|
||||||
"Invalid client address"
|
marketplace.withdrawFunds(slot.request, clientWithdrawRecipient.address)
|
||||||
)
|
).to.be.revertedWith("Invalid client address")
|
||||||
})
|
})
|
||||||
|
|
||||||
it("rejects withdraw when in wrong state", async function () {
|
it("rejects withdraw when in wrong state", async function () {
|
||||||
|
@ -610,29 +724,41 @@ describe("Marketplace", function () {
|
||||||
}
|
}
|
||||||
await waitUntilCancelled(request)
|
await waitUntilCancelled(request)
|
||||||
switchAccount(client)
|
switchAccount(client)
|
||||||
await expect(marketplace.withdrawFunds(slot.request)).to.be.revertedWith(
|
await expect(
|
||||||
"Invalid state"
|
marketplace.withdrawFunds(slot.request, clientWithdrawRecipient.address)
|
||||||
)
|
).to.be.revertedWith("Invalid state")
|
||||||
})
|
})
|
||||||
|
|
||||||
it("emits event once request is cancelled", async function () {
|
it("emits event once request is cancelled", async function () {
|
||||||
await waitUntilCancelled(request)
|
await waitUntilCancelled(request)
|
||||||
switchAccount(client)
|
switchAccount(client)
|
||||||
await expect(marketplace.withdrawFunds(slot.request))
|
await expect(
|
||||||
|
marketplace.withdrawFunds(slot.request, clientWithdrawRecipient.address)
|
||||||
|
)
|
||||||
.to.emit(marketplace, "RequestCancelled")
|
.to.emit(marketplace, "RequestCancelled")
|
||||||
.withArgs(requestId(request))
|
.withArgs(requestId(request))
|
||||||
})
|
})
|
||||||
|
|
||||||
it("withdraws to the client", async function () {
|
it("withdraws to the client payout address", async function () {
|
||||||
await waitUntilCancelled(request)
|
await waitUntilCancelled(request)
|
||||||
switchAccount(client)
|
switchAccount(client)
|
||||||
const startBalance = await token.balanceOf(client.address)
|
const startBalanceClient = await token.balanceOf(client.address)
|
||||||
await marketplace.withdrawFunds(slot.request)
|
const startBalancePayout = await token.balanceOf(
|
||||||
const endBalance = await token.balanceOf(client.address)
|
clientWithdrawRecipient.address
|
||||||
expect(endBalance - startBalance).to.equal(price(request))
|
)
|
||||||
|
await marketplace.withdrawFunds(
|
||||||
|
slot.request,
|
||||||
|
clientWithdrawRecipient.address
|
||||||
|
)
|
||||||
|
const endBalanceClient = await token.balanceOf(client.address)
|
||||||
|
const endBalancePayout = await token.balanceOf(
|
||||||
|
clientWithdrawRecipient.address
|
||||||
|
)
|
||||||
|
expect(endBalanceClient).to.equal(startBalanceClient)
|
||||||
|
expect(endBalancePayout - startBalancePayout).to.equal(price(request))
|
||||||
})
|
})
|
||||||
|
|
||||||
it("withdraws to the client for cancelled requests lowered by hosts payout", async function () {
|
it("withdraws to the client payout address for cancelled requests lowered by hosts payout", async function () {
|
||||||
// Lets advance the time more into the expiry window
|
// Lets advance the time more into the expiry window
|
||||||
const filledAt = (await currentTime()) + Math.floor(request.expiry / 3)
|
const filledAt = (await currentTime()) + Math.floor(request.expiry / 3)
|
||||||
const expiresAt = (
|
const expiresAt = (
|
||||||
|
@ -642,14 +768,17 @@ describe("Marketplace", function () {
|
||||||
await advanceTimeToForNextBlock(filledAt)
|
await advanceTimeToForNextBlock(filledAt)
|
||||||
await marketplace.fillSlot(slot.request, slot.index, proof)
|
await marketplace.fillSlot(slot.request, slot.index, proof)
|
||||||
await waitUntilCancelled(request)
|
await waitUntilCancelled(request)
|
||||||
const expectedPartialHostPayout =
|
const expectedPartialhostRewardRecipient =
|
||||||
(expiresAt - filledAt) * request.ask.reward
|
(expiresAt - filledAt) * request.ask.reward
|
||||||
|
|
||||||
switchAccount(client)
|
switchAccount(client)
|
||||||
await marketplace.withdrawFunds(slot.request)
|
await marketplace.withdrawFunds(
|
||||||
const endBalance = await token.balanceOf(client.address)
|
slot.request,
|
||||||
expect(ACCOUNT_STARTING_BALANCE - endBalance).to.equal(
|
clientWithdrawRecipient.address
|
||||||
expectedPartialHostPayout
|
)
|
||||||
|
const endBalance = await token.balanceOf(clientWithdrawRecipient.address)
|
||||||
|
expect(endBalance - ACCOUNT_STARTING_BALANCE).to.equal(
|
||||||
|
price(request) - expectedPartialhostRewardRecipient
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
@ -678,7 +807,10 @@ describe("Marketplace", function () {
|
||||||
it("remains 'Cancelled' when client withdraws funds", async function () {
|
it("remains 'Cancelled' when client withdraws funds", async function () {
|
||||||
await waitUntilCancelled(request)
|
await waitUntilCancelled(request)
|
||||||
switchAccount(client)
|
switchAccount(client)
|
||||||
await marketplace.withdrawFunds(slot.request)
|
await marketplace.withdrawFunds(
|
||||||
|
slot.request,
|
||||||
|
clientWithdrawRecipient.address
|
||||||
|
)
|
||||||
expect(await marketplace.requestState(slot.request)).to.equal(Cancelled)
|
expect(await marketplace.requestState(slot.request)).to.equal(Cancelled)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -1031,7 +1163,10 @@ describe("Marketplace", function () {
|
||||||
it("removes request from list when funds are withdrawn", async function () {
|
it("removes request from list when funds are withdrawn", async function () {
|
||||||
await marketplace.requestStorage(request)
|
await marketplace.requestStorage(request)
|
||||||
await waitUntilCancelled(request)
|
await waitUntilCancelled(request)
|
||||||
await marketplace.withdrawFunds(requestId(request))
|
await marketplace.withdrawFunds(
|
||||||
|
requestId(request),
|
||||||
|
clientWithdrawRecipient.address
|
||||||
|
)
|
||||||
expect(await marketplace.myRequests()).to.deep.equal([])
|
expect(await marketplace.myRequests()).to.deep.equal([])
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
@ -49,10 +49,41 @@ async function waitUntilSlotFailed(contract, request, slot) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function patchOverloads(contract) {
|
||||||
|
contract.freeSlot = async (slotId, rewardRecipient, collateralRecipient) => {
|
||||||
|
const logicalXor = (a, b) => (a || b) && !(a && b)
|
||||||
|
if (logicalXor(rewardRecipient, collateralRecipient)) {
|
||||||
|
// XOR, if exactly one is truthy
|
||||||
|
throw new Error(
|
||||||
|
"Invalid freeSlot overload, you must specify both `rewardRecipient` and `collateralRecipient` or neither."
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!rewardRecipient && !collateralRecipient) {
|
||||||
|
// calls `freeSlot` overload without `rewardRecipient` and `collateralRecipient`
|
||||||
|
const fn = contract["freeSlot(bytes32)"]
|
||||||
|
return await fn(slotId)
|
||||||
|
}
|
||||||
|
|
||||||
|
const fn = contract["freeSlot(bytes32,address,address)"]
|
||||||
|
return await fn(slotId, rewardRecipient, collateralRecipient)
|
||||||
|
}
|
||||||
|
contract.withdrawFunds = async (requestId, withdrawRecipient) => {
|
||||||
|
if (!withdrawRecipient) {
|
||||||
|
// calls `withdrawFunds` overload without `withdrawRecipient`
|
||||||
|
const fn = contract["withdrawFunds(bytes32)"]
|
||||||
|
return await fn(requestId)
|
||||||
|
}
|
||||||
|
const fn = contract["withdrawFunds(bytes32,address)"]
|
||||||
|
return await fn(requestId, withdrawRecipient)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
waitUntilCancelled,
|
waitUntilCancelled,
|
||||||
waitUntilStarted,
|
waitUntilStarted,
|
||||||
waitUntilFinished,
|
waitUntilFinished,
|
||||||
waitUntilFailed,
|
waitUntilFailed,
|
||||||
waitUntilSlotFailed,
|
waitUntilSlotFailed,
|
||||||
|
patchOverloads,
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue