feat: collateral per slot (#44)

This commit is contained in:
Adam Uhlíř 2023-03-08 12:02:34 +01:00 committed by GitHub
parent fcc28b3931
commit dfdbd16d5b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 179 additions and 360 deletions

16
.editorconfig Normal file
View File

@ -0,0 +1,16 @@
# EditorConfig is awesome: https://EditorConfig.org
# top-most EditorConfig file
root = true
# Unix-style newlines with a newline ending every file
[*]
end_of_line = lf
insert_final_newline = true
# Matches multiple files with brace expansion notation
# Set default charset
[{*.js, *.sol}]
charset = utf-8
indent_style = space
indent_size = 2

View File

@ -1,77 +0,0 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
abstract contract Collateral {
IERC20 public immutable token;
CollateralFunds private _funds;
mapping(address => uint256) private _balances;
constructor(IERC20 token_) collateralInvariant {
token = token_;
}
function balanceOf(address account) public view returns (uint256) {
return _balances[account];
}
function _add(address account, uint256 amount) private {
_balances[account] += amount;
_funds.balance += amount;
}
function _subtract(address account, uint256 amount) private {
_balances[account] -= amount;
_funds.balance -= amount;
}
function _transferFrom(address sender, uint256 amount) internal {
address receiver = address(this);
require(token.transferFrom(sender, receiver, amount), "Transfer failed");
}
function deposit(uint256 amount) public collateralInvariant {
_transferFrom(msg.sender, amount);
_funds.deposited += amount;
_add(msg.sender, amount);
}
function _isWithdrawAllowed() internal virtual returns (bool);
function withdraw() public collateralInvariant {
require(_isWithdrawAllowed(), "Account locked");
uint256 amount = balanceOf(msg.sender);
_funds.withdrawn += amount;
_subtract(msg.sender, amount);
assert(token.transfer(msg.sender, amount));
}
function _slash(
address account,
uint256 percentage
) internal collateralInvariant {
uint256 amount = (balanceOf(account) * percentage) / 100;
_funds.slashed += amount;
_subtract(account, amount);
}
modifier collateralInvariant() {
CollateralFunds memory oldFunds = _funds;
_;
assert(_funds.deposited >= oldFunds.deposited);
assert(_funds.withdrawn >= oldFunds.withdrawn);
assert(_funds.slashed >= oldFunds.slashed);
assert(
_funds.deposited == _funds.balance + _funds.withdrawn + _funds.slashed
);
}
struct CollateralFunds {
uint256 balance;
uint256 deposited;
uint256 withdrawn;
uint256 slashed;
}
}

View File

@ -9,7 +9,6 @@ struct MarketplaceConfig {
} }
struct CollateralConfig { struct CollateralConfig {
uint256 initialAmount; // amount of collateral necessary to fill a slot
uint256 minimumAmount; // frees slot when collateral drops below this minimum uint256 minimumAmount; // frees slot when collateral drops below this minimum
uint256 slashCriterion; // amount of proofs missed that lead to slashing uint256 slashCriterion; // amount of proofs missed that lead to slashing
uint256 slashPercentage; // percentage of the collateral that is slashed uint256 slashPercentage; // percentage of the collateral that is slashed

View File

@ -6,20 +6,20 @@ import "@openzeppelin/contracts/utils/math/Math.sol";
import "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; import "@openzeppelin/contracts/utils/structs/EnumerableSet.sol";
import "./Configuration.sol"; import "./Configuration.sol";
import "./Requests.sol"; import "./Requests.sol";
import "./Collateral.sol";
import "./Proofs.sol"; import "./Proofs.sol";
import "./StateRetrieval.sol"; import "./StateRetrieval.sol";
contract Marketplace is Collateral, Proofs, StateRetrieval { contract Marketplace is Proofs, StateRetrieval {
using EnumerableSet for EnumerableSet.Bytes32Set; using EnumerableSet for EnumerableSet.Bytes32Set;
using Requests for Request; using Requests for Request;
IERC20 public immutable token;
MarketplaceConfig public config; MarketplaceConfig public config;
MarketplaceFunds private _funds; MarketplaceFunds private _funds;
mapping(RequestId => Request) private _requests; mapping(RequestId => Request) private _requests;
mapping(RequestId => RequestContext) private _requestContexts; mapping(RequestId => RequestContext) private _requestContexts;
mapping(SlotId => Slot) private _slots; mapping(SlotId => Slot) internal _slots;
struct RequestContext { struct RequestContext {
RequestState state; RequestState state;
@ -31,20 +31,22 @@ contract Marketplace is Collateral, Proofs, StateRetrieval {
struct Slot { struct Slot {
SlotState state; SlotState state;
RequestId requestId; RequestId requestId;
/// @notice Tracks the current amount of host's collateral that is to be payed out at the end of Slot's lifespan.
/// @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
uint256 currentCollateral;
address host; address host;
} }
constructor( constructor(
IERC20 token, IERC20 token_,
MarketplaceConfig memory configuration MarketplaceConfig memory configuration
) Collateral(token) Proofs(configuration.proofs) marketplaceInvariant { ) Proofs(configuration.proofs) marketplaceInvariant {
token = token_;
config = configuration; config = configuration;
} }
function _isWithdrawAllowed() internal view override returns (bool) {
return !_hasSlots(msg.sender);
}
function requestStorage( function requestStorage(
Request calldata request Request calldata request
) public marketplaceInvariant { ) public marketplaceInvariant {
@ -80,11 +82,6 @@ contract Marketplace is Collateral, Proofs, StateRetrieval {
require(slotState(slotId) == SlotState.Free, "Slot is not free"); require(slotState(slotId) == SlotState.Free, "Slot is not free");
require(
balanceOf(msg.sender) >= config.collateral.initialAmount,
"Insufficient collateral"
);
_startRequiringProofs(slotId, request.ask.proofProbability); _startRequiringProofs(slotId, request.ask.proofProbability);
submitProof(slotId, proof); submitProof(slotId, proof);
@ -93,6 +90,13 @@ contract Marketplace is Collateral, Proofs, StateRetrieval {
RequestContext storage context = _requestContexts[requestId]; RequestContext storage context = _requestContexts[requestId];
context.slotsFilled += 1; context.slotsFilled += 1;
// Collect collateral
uint256 collateralAmount = request.ask.collateral;
_transferFrom(msg.sender, collateralAmount);
_funds.received += collateralAmount;
_funds.balance += collateralAmount;
slot.currentCollateral = collateralAmount;
_addToMySlots(slot.host, slotId); _addToMySlots(slot.host, slotId);
emit SlotFilled(requestId, slotIndex, slotId); emit SlotFilled(requestId, slotIndex, slotId);
@ -108,6 +112,7 @@ contract Marketplace is Collateral, Proofs, StateRetrieval {
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);
} else if (state == SlotState.Failed) { } else if (state == SlotState.Failed) {
@ -120,11 +125,15 @@ contract Marketplace is Collateral, Proofs, StateRetrieval {
function markProofAsMissing(SlotId slotId, Period period) public { function markProofAsMissing(SlotId slotId, Period period) public {
require(slotState(slotId) == SlotState.Filled, "Slot not accepting proofs"); require(slotState(slotId) == SlotState.Filled, "Slot not accepting proofs");
_markProofAsMissing(slotId, period); _markProofAsMissing(slotId, period);
address host = getHost(slotId); Slot storage slot = _slots[slotId];
if (missingProofs(slotId) % config.collateral.slashCriterion == 0) {
_slash(host, config.collateral.slashPercentage);
if (balanceOf(host) < config.collateral.minimumAmount) { if (missingProofs(slotId) % config.collateral.slashCriterion == 0) {
uint256 slashedAmount = (slot.currentCollateral * config.collateral.slashPercentage) / 100;
slot.currentCollateral -= slashedAmount;
_funds.slashed += slashedAmount;
_funds.balance -= slashedAmount;
if (slot.currentCollateral < config.collateral.minimumAmount) {
// When the collateral drops below the minimum threshold, the slot // When the collateral drops below the minimum threshold, the slot
// needs to be freed so that there is enough remaining collateral to be // needs to be freed so that there is enough remaining collateral to be
// distributed for repairs and rewards (with any leftover to be burnt). // distributed for repairs and rewards (with any leftover to be burnt).
@ -138,16 +147,9 @@ contract Marketplace is Collateral, Proofs, StateRetrieval {
RequestId requestId = slot.requestId; RequestId requestId = slot.requestId;
RequestContext storage context = _requestContexts[requestId]; RequestContext storage context = _requestContexts[requestId];
// TODO: burn host's slot collateral except for repair costs + mark proof
// missing reward
// Slot collateral is not yet implemented as the design decision was
// not finalised.
_removeFromMySlots(slot.host, slotId); _removeFromMySlots(slot.host, slotId);
slot.state = SlotState.Free; delete _slots[slotId];
slot.host = address(0);
slot.requestId = RequestId.wrap(0);
context.slotsFilled -= 1; context.slotsFilled -= 1;
emit SlotFreed(requestId, slotId); emit SlotFreed(requestId, slotId);
@ -161,8 +163,6 @@ contract Marketplace is Collateral, Proofs, StateRetrieval {
context.endsAt = block.timestamp - 1; context.endsAt = block.timestamp - 1;
emit RequestFailed(requestId); emit RequestFailed(requestId);
// TODO: burn all remaining slot collateral (note: slot collateral not
// yet implemented)
// TODO: send client remaining funds // TODO: send client remaining funds
} }
} }
@ -179,7 +179,7 @@ contract Marketplace is Collateral, Proofs, StateRetrieval {
_removeFromMySlots(slot.host, slotId); _removeFromMySlots(slot.host, slotId);
uint256 amount = _requests[requestId].pricePerSlot(); uint256 amount = _requests[requestId].pricePerSlot() + slot.currentCollateral;
_funds.sent += amount; _funds.sent += amount;
_funds.balance -= amount; _funds.balance -= amount;
slot.state = SlotState.Paid; slot.state = SlotState.Paid;
@ -212,10 +212,6 @@ contract Marketplace is Collateral, Proofs, StateRetrieval {
require(token.transfer(msg.sender, amount), "Withdraw failed"); require(token.transfer(msg.sender, amount), "Withdraw failed");
} }
function getHost(SlotId slotId) public view returns (address) {
return _slots[slotId].host;
}
function getRequestFromSlotId(SlotId slotId) function getRequestFromSlotId(SlotId slotId)
public public
view view
@ -252,6 +248,10 @@ contract Marketplace is Collateral, Proofs, StateRetrieval {
} }
} }
function getHost(SlotId slotId) public view returns (address) {
return _slots[slotId].host;
}
function requestState( function requestState(
RequestId requestId RequestId requestId
) public view requestIsKnown(requestId) returns (RequestState) { ) public view requestIsKnown(requestId) returns (RequestState) {
@ -291,6 +291,11 @@ contract Marketplace is Collateral, Proofs, StateRetrieval {
return slot.state; return slot.state;
} }
function _transferFrom(address sender, uint256 amount) internal {
address receiver = address(this);
require(token.transferFrom(sender, receiver, amount), "Transfer failed");
}
event StorageRequested(RequestId requestId, Ask ask); event StorageRequested(RequestId requestId, Ask ask);
event RequestFulfilled(RequestId indexed requestId); event RequestFulfilled(RequestId indexed requestId);
event RequestFailed(RequestId indexed requestId); event RequestFailed(RequestId indexed requestId);
@ -307,12 +312,14 @@ contract Marketplace is Collateral, Proofs, StateRetrieval {
_; _;
assert(_funds.received >= oldFunds.received); assert(_funds.received >= oldFunds.received);
assert(_funds.sent >= oldFunds.sent); assert(_funds.sent >= oldFunds.sent);
assert(_funds.received == _funds.balance + _funds.sent); assert(_funds.slashed >= oldFunds.slashed);
assert(_funds.received == _funds.balance + _funds.sent + _funds.slashed);
} }
struct MarketplaceFunds { struct MarketplaceFunds {
uint256 balance; uint256 balance;
uint256 received; uint256 received;
uint256 sent; uint256 sent;
uint256 slashed;
} }
} }

View File

@ -8,7 +8,7 @@ struct Request {
address client; address client;
Ask ask; Ask ask;
Content content; Content content;
uint256 expiry; // time at which this request expires uint256 expiry; // time at which this request timeouts if all slots are not filled and is pronounced cancelled
bytes32 nonce; // random nonce to differentiate between similar requests bytes32 nonce; // random nonce to differentiate between similar requests
} }
@ -18,6 +18,7 @@ struct Ask {
uint256 duration; // how long content should be stored (in seconds) uint256 duration; // how long content should be stored (in seconds)
uint256 proofProbability; // how often storage proofs are required uint256 proofProbability; // how often storage proofs are required
uint256 reward; // amount of tokens paid per second per slot to hosts uint256 reward; // amount of tokens paid per second per slot to hosts
uint256 collateral; // amount of tokens required to be deposited by the hosts in order to fill the slot
uint64 maxSlotLoss; // Max slots that can be lost without data considered to be lost uint64 maxSlotLoss; // Max slots that can be lost without data considered to be lost
} }

View File

@ -1,18 +0,0 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "./Collateral.sol";
// exposes internal functions for testing
contract TestCollateral is Collateral {
// solhint-disable-next-line no-empty-blocks
constructor(IERC20 token) Collateral(token) {}
function slash(address account, uint256 percentage) public {
_slash(account, percentage);
}
function _isWithdrawAllowed() internal pure override returns (bool) {
return true;
}
}

View File

@ -15,4 +15,8 @@ contract TestMarketplace is Marketplace {
function forciblyFreeSlot(SlotId slotId) public { function forciblyFreeSlot(SlotId slotId) public {
_forciblyFreeSlot(slotId); _forciblyFreeSlot(slotId);
} }
function getSlotCollateral(SlotId slotId) public view returns (uint256) {
return _slots[slotId].currentCollateral;
}
} }

View File

@ -2,7 +2,6 @@ async function deployMarketplace({ deployments, getNamedAccounts }) {
const token = await deployments.get("TestToken") const token = await deployments.get("TestToken")
const configuration = { const configuration = {
collateral: { collateral: {
initialAmount: 100,
minimumAmount: 40, minimumAmount: 40,
slashCriterion: 3, slashCriterion: 3,
slashPercentage: 10, slashPercentage: 10,

44
package-lock.json generated
View File

@ -14,8 +14,8 @@
"chai": "^4.3.7", "chai": "^4.3.7",
"ethereum-waffle": "^3.4.4", "ethereum-waffle": "^3.4.4",
"ethers": "^5.7.2", "ethers": "^5.7.2",
"hardhat": "^2.12.5", "hardhat": "^2.12.7",
"hardhat-deploy": "^0.11.22", "hardhat-deploy": "^0.11.23",
"hardhat-deploy-ethers": "^0.3.0-beta.13", "hardhat-deploy-ethers": "^0.3.0-beta.13",
"prettier": "^2.8.2", "prettier": "^2.8.2",
"prettier-plugin-solidity": "^1.1.1", "prettier-plugin-solidity": "^1.1.1",
@ -15670,8 +15670,6 @@
}, },
"node_modules/ganache-core/node_modules/keccak": { "node_modules/ganache-core/node_modules/keccak": {
"version": "3.0.1", "version": "3.0.1",
"resolved": "https://registry.npmjs.org/keccak/-/keccak-3.0.1.tgz",
"integrity": "sha512-epq90L9jlFWCW7+pQa6JOnKn2Xgl2mtI664seYR6MHskvI9agt7AnDqmAlp9TqU4/caMYbA08Hi5DMZAl5zdkA==",
"dev": true, "dev": true,
"hasInstallScript": true, "hasInstallScript": true,
"inBundle": true, "inBundle": true,
@ -16245,8 +16243,6 @@
}, },
"node_modules/ganache-core/node_modules/node-addon-api": { "node_modules/ganache-core/node_modules/node-addon-api": {
"version": "2.0.2", "version": "2.0.2",
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-2.0.2.tgz",
"integrity": "sha512-Ntyt4AIXyaLIuMHF6IOoTakB3K+RWxwtsHNRxllEoA6vPwP9o4866g6YWDLUdnucilZhmkxiHwHr11gAENw+QA==",
"dev": true, "dev": true,
"inBundle": true, "inBundle": true,
"license": "MIT" "license": "MIT"
@ -16261,8 +16257,6 @@
}, },
"node_modules/ganache-core/node_modules/node-gyp-build": { "node_modules/ganache-core/node_modules/node-gyp-build": {
"version": "4.2.3", "version": "4.2.3",
"resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.2.3.tgz",
"integrity": "sha512-MN6ZpzmfNCRM+3t57PTJHgHyw/h4OWnZ6mR8P5j/uZtqQr46RRuDE/P+g3n0YR/AiYXeWixZZzaip77gdICfRg==",
"dev": true, "dev": true,
"inBundle": true, "inBundle": true,
"license": "MIT", "license": "MIT",
@ -19517,9 +19511,9 @@
} }
}, },
"node_modules/hardhat": { "node_modules/hardhat": {
"version": "2.12.5", "version": "2.12.7",
"resolved": "https://registry.npmjs.org/hardhat/-/hardhat-2.12.5.tgz", "resolved": "https://registry.npmjs.org/hardhat/-/hardhat-2.12.7.tgz",
"integrity": "sha512-f/t7+hLlhsnQZ6LDXyV+8rHGRZFZY1sgFvgrwr9fBjMdGp1Bu6hHq1KXS4/VFZfZcVdL1DAWWEkryinZhqce+A==", "integrity": "sha512-voWoN6zn5d8BOEaczSyK/1PyfdeOeI3SbGCFb36yCHTJUt6OIqLb+ZDX30VhA1UsYKzLqG7UnWl3fKJUuANc6A==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"@ethersproject/abi": "^5.1.2", "@ethersproject/abi": "^5.1.2",
@ -19569,7 +19563,7 @@
"source-map-support": "^0.5.13", "source-map-support": "^0.5.13",
"stacktrace-parser": "^0.1.10", "stacktrace-parser": "^0.1.10",
"tsort": "0.0.1", "tsort": "0.0.1",
"undici": "^5.4.0", "undici": "^5.14.0",
"uuid": "^8.3.2", "uuid": "^8.3.2",
"ws": "^7.4.6" "ws": "^7.4.6"
}, },
@ -19593,9 +19587,9 @@
} }
}, },
"node_modules/hardhat-deploy": { "node_modules/hardhat-deploy": {
"version": "0.11.22", "version": "0.11.23",
"resolved": "https://registry.npmjs.org/hardhat-deploy/-/hardhat-deploy-0.11.22.tgz", "resolved": "https://registry.npmjs.org/hardhat-deploy/-/hardhat-deploy-0.11.23.tgz",
"integrity": "sha512-ZhHVNB7Jo2l8Is+KIAk9F8Q3d7pptyiX+nsNbIFXztCz81kaP+6kxNODRBqRCy7SOD3It4+iKCL6tWsPAA/jVQ==", "integrity": "sha512-9F+sDRX79D/oV1cUEE0k2h5LiccrnzXEtrMofL5PTVDCJfUnRvhQqCRi4NhcYmxf2+MBkOIJv5KyzP0lz6ojTw==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"@types/qs": "^6.9.7", "@types/qs": "^6.9.7",
@ -33185,8 +33179,6 @@
}, },
"keccak": { "keccak": {
"version": "3.0.1", "version": "3.0.1",
"resolved": "https://registry.npmjs.org/keccak/-/keccak-3.0.1.tgz",
"integrity": "sha512-epq90L9jlFWCW7+pQa6JOnKn2Xgl2mtI664seYR6MHskvI9agt7AnDqmAlp9TqU4/caMYbA08Hi5DMZAl5zdkA==",
"bundled": true, "bundled": true,
"dev": true, "dev": true,
"requires": { "requires": {
@ -33618,8 +33610,6 @@
}, },
"node-addon-api": { "node-addon-api": {
"version": "2.0.2", "version": "2.0.2",
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-2.0.2.tgz",
"integrity": "sha512-Ntyt4AIXyaLIuMHF6IOoTakB3K+RWxwtsHNRxllEoA6vPwP9o4866g6YWDLUdnucilZhmkxiHwHr11gAENw+QA==",
"bundled": true, "bundled": true,
"dev": true "dev": true
}, },
@ -33629,8 +33619,6 @@
}, },
"node-gyp-build": { "node-gyp-build": {
"version": "4.2.3", "version": "4.2.3",
"resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.2.3.tgz",
"integrity": "sha512-MN6ZpzmfNCRM+3t57PTJHgHyw/h4OWnZ6mR8P5j/uZtqQr46RRuDE/P+g3n0YR/AiYXeWixZZzaip77gdICfRg==",
"bundled": true, "bundled": true,
"dev": true "dev": true
}, },
@ -36069,9 +36057,9 @@
} }
}, },
"hardhat": { "hardhat": {
"version": "2.12.5", "version": "2.12.7",
"resolved": "https://registry.npmjs.org/hardhat/-/hardhat-2.12.5.tgz", "resolved": "https://registry.npmjs.org/hardhat/-/hardhat-2.12.7.tgz",
"integrity": "sha512-f/t7+hLlhsnQZ6LDXyV+8rHGRZFZY1sgFvgrwr9fBjMdGp1Bu6hHq1KXS4/VFZfZcVdL1DAWWEkryinZhqce+A==", "integrity": "sha512-voWoN6zn5d8BOEaczSyK/1PyfdeOeI3SbGCFb36yCHTJUt6OIqLb+ZDX30VhA1UsYKzLqG7UnWl3fKJUuANc6A==",
"dev": true, "dev": true,
"requires": { "requires": {
"@ethersproject/abi": "^5.1.2", "@ethersproject/abi": "^5.1.2",
@ -36121,7 +36109,7 @@
"source-map-support": "^0.5.13", "source-map-support": "^0.5.13",
"stacktrace-parser": "^0.1.10", "stacktrace-parser": "^0.1.10",
"tsort": "0.0.1", "tsort": "0.0.1",
"undici": "^5.4.0", "undici": "^5.14.0",
"uuid": "^8.3.2", "uuid": "^8.3.2",
"ws": "^7.4.6" "ws": "^7.4.6"
}, },
@ -36197,9 +36185,9 @@
} }
}, },
"hardhat-deploy": { "hardhat-deploy": {
"version": "0.11.22", "version": "0.11.23",
"resolved": "https://registry.npmjs.org/hardhat-deploy/-/hardhat-deploy-0.11.22.tgz", "resolved": "https://registry.npmjs.org/hardhat-deploy/-/hardhat-deploy-0.11.23.tgz",
"integrity": "sha512-ZhHVNB7Jo2l8Is+KIAk9F8Q3d7pptyiX+nsNbIFXztCz81kaP+6kxNODRBqRCy7SOD3It4+iKCL6tWsPAA/jVQ==", "integrity": "sha512-9F+sDRX79D/oV1cUEE0k2h5LiccrnzXEtrMofL5PTVDCJfUnRvhQqCRi4NhcYmxf2+MBkOIJv5KyzP0lz6ojTw==",
"dev": true, "dev": true,
"requires": { "requires": {
"@types/qs": "^6.9.7", "@types/qs": "^6.9.7",

View File

@ -15,8 +15,8 @@
"chai": "^4.3.7", "chai": "^4.3.7",
"ethereum-waffle": "^3.4.4", "ethereum-waffle": "^3.4.4",
"ethers": "^5.7.2", "ethers": "^5.7.2",
"hardhat": "^2.12.5", "hardhat": "^2.12.7",
"hardhat-deploy": "^0.11.22", "hardhat-deploy": "^0.11.23",
"hardhat-deploy-ethers": "^0.3.0-beta.13", "hardhat-deploy-ethers": "^0.3.0-beta.13",
"prettier": "^2.8.2", "prettier": "^2.8.2",
"prettier-plugin-solidity": "^1.1.1", "prettier-plugin-solidity": "^1.1.1",

View File

@ -1,92 +0,0 @@
const { expect } = require("chai")
describe("Collateral", function () {
let collateral, token
let account0, account1
beforeEach(async function () {
let Collateral = await ethers.getContractFactory("TestCollateral")
let TestToken = await ethers.getContractFactory("TestToken")
token = await TestToken.deploy()
collateral = await Collateral.deploy(token.address)
;[account0, account1] = await ethers.getSigners()
await token.mint(account0.address, 1000)
await token.mint(account1.address, 1000)
})
it("assigns zero collateral by default", async function () {
expect(await collateral.balanceOf(account0.address)).to.equal(0)
expect(await collateral.balanceOf(account1.address)).to.equal(0)
})
describe("depositing", function () {
beforeEach(async function () {
await token.connect(account0).approve(collateral.address, 100)
await token.connect(account1).approve(collateral.address, 100)
})
it("updates the amount of collateral", async function () {
await collateral.connect(account0).deposit(40)
await collateral.connect(account1).deposit(2)
expect(await collateral.balanceOf(account0.address)).to.equal(40)
expect(await collateral.balanceOf(account1.address)).to.equal(2)
})
it("transfers tokens to the contract", async function () {
let before = await token.balanceOf(collateral.address)
await collateral.deposit(42)
let after = await token.balanceOf(collateral.address)
expect(after - before).to.equal(42)
})
it("fails when token transfer fails", async function () {
let allowed = await token.allowance(account0.address, collateral.address)
let invalidAmount = allowed.toNumber() + 1
await expect(collateral.deposit(invalidAmount)).to.be.revertedWith(
"ERC20: insufficient allowance"
)
})
})
describe("withdrawing", function () {
beforeEach(async function () {
await token.connect(account0).approve(collateral.address, 100)
await token.connect(account1).approve(collateral.address, 100)
await collateral.connect(account0).deposit(40)
await collateral.connect(account1).deposit(2)
})
it("updates the amount of collateral", async function () {
await collateral.connect(account0).withdraw()
expect(await collateral.balanceOf(account0.address)).to.equal(0)
expect(await collateral.balanceOf(account1.address)).to.equal(2)
await collateral.connect(account1).withdraw()
expect(await collateral.balanceOf(account0.address)).to.equal(0)
expect(await collateral.balanceOf(account1.address)).to.equal(0)
})
it("transfers balance to owner", async function () {
let balance = await collateral.balanceOf(account0.address)
let before = await token.balanceOf(account0.address)
await collateral.withdraw()
let after = await token.balanceOf(account0.address)
expect(after - before).to.equal(balance)
})
})
describe("slashing", function () {
beforeEach(async function () {
await token.connect(account0).approve(collateral.address, 1000)
await token.connect(account1).approve(collateral.address, 1000)
await collateral.connect(account0).deposit(1000)
await collateral.connect(account1).deposit(1000)
})
it("reduces the amount of collateral by a percentage", async function () {
await collateral.slash(account0.address, 10)
await collateral.slash(account1.address, 5)
expect(await collateral.balanceOf(account0.address)).to.equal(900)
expect(await collateral.balanceOf(account1.address)).to.equal(950)
})
})
})

View File

@ -118,14 +118,13 @@ describe("Marketplace", function () {
}) })
}) })
describe("filling a slot", function () { describe("filling a slot with collateral", function () {
beforeEach(async function () { beforeEach(async function () {
switchAccount(client) switchAccount(client)
await token.approve(marketplace.address, price(request)) await token.approve(marketplace.address, price(request))
await marketplace.requestStorage(request) await marketplace.requestStorage(request)
switchAccount(host) switchAccount(host)
await token.approve(marketplace.address, config.collateral.initialAmount) await token.approve(marketplace.address, request.ask.collateral)
await marketplace.deposit(config.collateral.initialAmount)
}) })
it("emits event when slot is filled", async function () { it("emits event when slot is filled", async function () {
@ -160,16 +159,6 @@ describe("Marketplace", function () {
).to.be.revertedWith("Invalid proof") ).to.be.revertedWith("Invalid proof")
}) })
it("is rejected when collateral is insufficient", async function () {
let insufficient = config.collateral.initialAmount - 1
await marketplace.withdraw()
await token.approve(marketplace.address, insufficient)
await marketplace.deposit(insufficient)
await expect(
marketplace.fillSlot(slot.request, slot.index, proof)
).to.be.revertedWith("Insufficient collateral")
})
it("is rejected when slot already filled", async function () { it("is rejected when slot already filled", async function () {
await marketplace.fillSlot(slot.request, slot.index, proof) await marketplace.fillSlot(slot.request, slot.index, proof)
await expect( await expect(
@ -196,15 +185,15 @@ describe("Marketplace", function () {
}) })
it("is rejected when request is finished", async function () { it("is rejected when request is finished", async function () {
await waitUntilStarted(marketplace, request, proof) await waitUntilStarted(marketplace, request, proof, token)
await waitUntilFinished(marketplace, requestId(request)) await waitUntilFinished(marketplace, slot.request)
await expect( await expect(
marketplace.fillSlot(slot.request, slot.index, proof) marketplace.fillSlot(slot.request, slot.index, proof)
).to.be.revertedWith("Slot is not free") ).to.be.revertedWith("Slot is not free")
}) })
it("is rejected when request is failed", async function () { it("is rejected when request is failed", async function () {
await waitUntilStarted(marketplace, request, proof) await waitUntilStarted(marketplace, request, proof, token)
await waitUntilFailed(marketplace, request) await waitUntilFailed(marketplace, request)
await expect( await expect(
marketplace.fillSlot(slot.request, slot.index, proof) marketplace.fillSlot(slot.request, slot.index, proof)
@ -220,6 +209,8 @@ describe("Marketplace", function () {
it("fails when all slots are already filled", async function () { it("fails when all slots are already filled", async function () {
const lastSlot = request.ask.slots - 1 const lastSlot = request.ask.slots - 1
await token.approve(marketplace.address, request.ask.collateral * lastSlot)
await token.approve(marketplace.address, price(request) * lastSlot)
for (let i = 0; i <= lastSlot; i++) { for (let i = 0; i <= lastSlot; i++) {
await marketplace.fillSlot(slot.request, i, proof) await marketplace.fillSlot(slot.request, i, proof)
} }
@ -229,6 +220,31 @@ describe("Marketplace", function () {
}) })
}) })
describe("filling slot without collateral", function () {
beforeEach(async function () {
switchAccount(client)
await token.approve(marketplace.address, price(request))
await marketplace.requestStorage(request)
switchAccount(host)
})
it("is rejected when approved collateral is insufficient", async function () {
let insufficient = request.ask.collateral - 1
await token.approve(marketplace.address, insufficient)
await expect(
marketplace.fillSlot(slot.request, slot.index, proof)
).to.be.revertedWith("ERC20: insufficient allowance")
})
it("collects only requested collateral and not more", async function () {
await token.approve(marketplace.address, request.ask.collateral*2)
const startBalanace = await token.balanceOf(host.address)
await marketplace.fillSlot(slot.request, slot.index, proof)
const endBalance = await token.balanceOf(host.address)
expect(startBalanace-endBalance).to.eq(request.ask.collateral)
})
})
describe("request end", function () { describe("request end", function () {
var requestTime var requestTime
beforeEach(async function () { beforeEach(async function () {
@ -237,8 +253,7 @@ describe("Marketplace", function () {
await marketplace.requestStorage(request) await marketplace.requestStorage(request)
requestTime = await currentTime() requestTime = await currentTime()
switchAccount(host) switchAccount(host)
await token.approve(marketplace.address, config.collateral.initialAmount) await token.approve(marketplace.address, request.ask.collateral)
await marketplace.deposit(config.collateral.initialAmount)
}) })
it("sets the request end time to now + duration", async function () { it("sets the request end time to now + duration", async function () {
@ -249,7 +264,7 @@ describe("Marketplace", function () {
}) })
it("sets request end time to the past once failed", async function () { it("sets request end time to the past once failed", async function () {
await waitUntilStarted(marketplace, request, proof) await waitUntilStarted(marketplace, request, proof, token)
await waitUntilFailed(marketplace, request) await waitUntilFailed(marketplace, request)
let slot0 = { ...slot, index: request.ask.maxSlotLoss + 1 } let slot0 = { ...slot, index: request.ask.maxSlotLoss + 1 }
const now = await currentTime() const now = await currentTime()
@ -268,7 +283,7 @@ describe("Marketplace", function () {
}) })
it("checks that request end time is in the past once finished", async function () { it("checks that request end time is in the past once finished", async function () {
await waitUntilStarted(marketplace, request, proof) await waitUntilStarted(marketplace, request, proof, token)
await waitUntilFinished(marketplace, requestId(request)) await waitUntilFinished(marketplace, requestId(request))
const now = await currentTime() const now = await currentTime()
// in the process of calling currentTime and requestEnd, // in the process of calling currentTime and requestEnd,
@ -281,7 +296,8 @@ describe("Marketplace", function () {
}) })
describe("freeing a slot", function () { describe("freeing a slot", function () {
var id let id
beforeEach(async function () { beforeEach(async function () {
slot.index = 0 slot.index = 0
id = slotId(slot) id = slotId(slot)
@ -290,8 +306,7 @@ describe("Marketplace", function () {
await token.approve(marketplace.address, price(request)) await token.approve(marketplace.address, price(request))
await marketplace.requestStorage(request) await marketplace.requestStorage(request)
switchAccount(host) switchAccount(host)
await token.approve(marketplace.address, config.collateral.initialAmount) await token.approve(marketplace.address, request.ask.collateral)
await marketplace.deposit(config.collateral.initialAmount)
}) })
it("fails to free slot when slot not filled", async function () { it("fails to free slot when slot not filled", async function () {
@ -303,7 +318,7 @@ describe("Marketplace", function () {
}) })
it("can only be freed by the host occupying the slot", async function () { it("can only be freed by the host occupying the slot", async function () {
await waitUntilStarted(marketplace, request, proof) await waitUntilStarted(marketplace, request, proof, token)
switchAccount(client) switchAccount(client)
await expect(marketplace.freeSlot(id)).to.be.revertedWith( await expect(marketplace.freeSlot(id)).to.be.revertedWith(
"Slot filled by other host" "Slot filled by other host"
@ -311,12 +326,12 @@ describe("Marketplace", function () {
}) })
it("successfully frees slot", async function () { it("successfully frees slot", async function () {
await waitUntilStarted(marketplace, request, proof) await waitUntilStarted(marketplace, request, proof, token)
await expect(marketplace.freeSlot(id)).not.to.be.reverted await expect(marketplace.freeSlot(id)).not.to.be.reverted
}) })
it("emits event once slot is freed", async function () { it("emits event once slot is freed", async function () {
await waitUntilStarted(marketplace, request, proof) await waitUntilStarted(marketplace, request, proof, token)
await expect(await marketplace.freeSlot(id)) await expect(await marketplace.freeSlot(id))
.to.emit(marketplace, "SlotFreed") .to.emit(marketplace, "SlotFreed")
.withArgs(slot.request, id) .withArgs(slot.request, id)
@ -329,17 +344,16 @@ describe("Marketplace", function () {
await token.approve(marketplace.address, price(request)) await token.approve(marketplace.address, price(request))
await marketplace.requestStorage(request) await marketplace.requestStorage(request)
switchAccount(host) switchAccount(host)
await token.approve(marketplace.address, config.collateral.initialAmount) await token.approve(marketplace.address, request.ask.collateral)
await marketplace.deposit(config.collateral.initialAmount)
}) })
it("pays the host when contract has finished", async function () { it("pays the host when contract has finished and returns collateral", async function () {
await waitUntilStarted(marketplace, request, proof) await waitUntilStarted(marketplace, request, proof, token)
await waitUntilFinished(marketplace, requestId(request)) await waitUntilFinished(marketplace, requestId(request))
const startBalance = await token.balanceOf(host.address) const startBalance = await token.balanceOf(host.address)
await marketplace.freeSlot(slotId(slot)) await marketplace.freeSlot(slotId(slot))
const endBalance = await token.balanceOf(host.address) const endBalance = await token.balanceOf(host.address)
expect(endBalance - startBalance).to.equal(pricePerSlot(request)) expect(endBalance - startBalance).to.equal(pricePerSlot(request) + request.ask.collateral)
}) })
it("pays the host when contract was cancelled", async function () { it("pays the host when contract was cancelled", async function () {
@ -360,7 +374,7 @@ describe("Marketplace", function () {
}) })
it("can only be done once", async function () { it("can only be done once", async function () {
await waitUntilStarted(marketplace, request, proof) await waitUntilStarted(marketplace, request, proof, token)
await waitUntilFinished(marketplace, requestId(request)) await waitUntilFinished(marketplace, requestId(request))
await marketplace.freeSlot(slotId(slot)) await marketplace.freeSlot(slotId(slot))
await expect(marketplace.freeSlot(slotId(slot))).to.be.revertedWith( await expect(marketplace.freeSlot(slotId(slot))).to.be.revertedWith(
@ -369,7 +383,7 @@ describe("Marketplace", function () {
}) })
it("cannot be filled again", async function () { it("cannot be filled again", async function () {
await waitUntilStarted(marketplace, request, proof) await waitUntilStarted(marketplace, request, proof, token)
await waitUntilFinished(marketplace, requestId(request)) await waitUntilFinished(marketplace, requestId(request))
await marketplace.freeSlot(slotId(slot)) await marketplace.freeSlot(slotId(slot))
await expect(marketplace.fillSlot(slot.request, slot.index, proof)).to.be await expect(marketplace.fillSlot(slot.request, slot.index, proof)).to.be
@ -383,22 +397,25 @@ describe("Marketplace", function () {
await token.approve(marketplace.address, price(request)) await token.approve(marketplace.address, price(request))
await marketplace.requestStorage(request) await marketplace.requestStorage(request)
switchAccount(host) switchAccount(host)
await token.approve(marketplace.address, config.collateral.initialAmount) await token.approve(marketplace.address, request.ask.collateral)
await marketplace.deposit(config.collateral.initialAmount)
}) })
it("emits event when all slots are filled", async function () { it("emits event when all slots are filled", async function () {
const lastSlot = request.ask.slots - 1 const lastSlot = request.ask.slots - 1
await token.approve(marketplace.address, request.ask.collateral * lastSlot)
for (let i = 0; i < lastSlot; i++) { for (let i = 0; i < lastSlot; i++) {
await marketplace.fillSlot(slot.request, i, proof) await marketplace.fillSlot(slot.request, i, proof)
} }
await token.approve(marketplace.address, request.ask.collateral)
await expect(marketplace.fillSlot(slot.request, lastSlot, proof)) await expect(marketplace.fillSlot(slot.request, lastSlot, proof))
.to.emit(marketplace, "RequestFulfilled") .to.emit(marketplace, "RequestFulfilled")
.withArgs(requestId(request)) .withArgs(requestId(request))
}) })
it("sets state when all slots are filled", async function () { it("sets state when all slots are filled", async function () {
const lastSlot = request.ask.slots - 1 const slots = request.ask.slots
for (let i = 0; i <= lastSlot; i++) { await token.approve(marketplace.address, request.ask.collateral * slots)
for (let i = 0; i < slots; i++) {
await marketplace.fillSlot(slot.request, i, proof) await marketplace.fillSlot(slot.request, i, proof)
} }
await expect(await marketplace.requestState(slot.request)).to.equal( await expect(await marketplace.requestState(slot.request)).to.equal(
@ -407,6 +424,7 @@ describe("Marketplace", function () {
}) })
it("fails when all slots are already filled", async function () { it("fails when all slots are already filled", async function () {
const lastSlot = request.ask.slots - 1 const lastSlot = request.ask.slots - 1
await token.approve(marketplace.address, request.ask.collateral * (lastSlot + 1))
for (let i = 0; i <= lastSlot; i++) { for (let i = 0; i <= lastSlot; i++) {
await marketplace.fillSlot(slot.request, i, proof) await marketplace.fillSlot(slot.request, i, proof)
} }
@ -422,8 +440,7 @@ describe("Marketplace", function () {
await token.approve(marketplace.address, price(request)) await token.approve(marketplace.address, price(request))
await marketplace.requestStorage(request) await marketplace.requestStorage(request)
switchAccount(host) switchAccount(host)
await token.approve(marketplace.address, config.collateral.initialAmount) await token.approve(marketplace.address, request.ask.collateral)
await marketplace.deposit(config.collateral.initialAmount)
}) })
it("rejects withdraw when request not yet timed out", async function () { it("rejects withdraw when request not yet timed out", async function () {
@ -443,6 +460,7 @@ describe("Marketplace", function () {
it("rejects withdraw when in wrong state", async function () { it("rejects withdraw when in wrong state", async function () {
// fill all slots, should change state to RequestState.Started // fill all slots, should change state to RequestState.Started
const lastSlot = request.ask.slots - 1 const lastSlot = request.ask.slots - 1
await token.approve(marketplace.address, request.ask.collateral * (lastSlot + 1))
for (let i = 0; i <= lastSlot; i++) { for (let i = 0; i <= lastSlot; i++) {
await marketplace.fillSlot(slot.request, i, proof) await marketplace.fillSlot(slot.request, i, proof)
} }
@ -471,34 +489,6 @@ describe("Marketplace", function () {
}) })
}) })
describe("collateral locking", function () {
beforeEach(async function () {
switchAccount(client)
await token.approve(marketplace.address, price(request))
await marketplace.requestStorage(request)
switchAccount(host)
await token.approve(marketplace.address, config.collateral.initialAmount)
await marketplace.deposit(config.collateral.initialAmount)
})
it("locks collateral of host when it fills a slot", async function () {
await marketplace.fillSlot(slot.request, slot.index, proof)
await expect(marketplace.withdraw()).to.be.revertedWith("Account locked")
})
it("allows withdrawal when all slots are free", async function () {
let slot1 = { ...slot, index: 0 }
let slot2 = { ...slot, index: 1 }
await marketplace.fillSlot(slot1.request, slot1.index, proof)
await marketplace.fillSlot(slot2.request, slot2.index, proof)
await waitUntilFinished(marketplace, requestId(request))
await marketplace.freeSlot(slotId(slot1))
await expect(marketplace.withdraw()).to.be.revertedWith("Account locked")
await marketplace.freeSlot(slotId(slot2))
await expect(marketplace.withdraw()).not.to.be.reverted
})
})
describe("request state", function () { describe("request state", function () {
const { New, Cancelled, Started, Failed, Finished } = RequestState const { New, Cancelled, Started, Failed, Finished } = RequestState
@ -507,8 +497,7 @@ describe("Marketplace", function () {
await token.approve(marketplace.address, price(request)) await token.approve(marketplace.address, price(request))
await marketplace.requestStorage(request) await marketplace.requestStorage(request)
switchAccount(host) switchAccount(host)
await token.approve(marketplace.address, config.collateral.initialAmount) await token.approve(marketplace.address, request.ask.collateral)
await marketplace.deposit(config.collateral.initialAmount)
}) })
it("is 'New' initially", async function () { it("is 'New' initially", async function () {
@ -528,17 +517,18 @@ describe("Marketplace", function () {
}) })
it("changes to 'Started' once all slots are filled", async function () { it("changes to 'Started' once all slots are filled", async function () {
await waitUntilStarted(marketplace, request, proof) await waitUntilStarted(marketplace, request, proof, token)
expect(await marketplace.requestState(slot.request)).to.equal(Started) expect(await marketplace.requestState(slot.request)).to.equal(Started)
}) })
it("changes to 'Failed' once too many slots are freed", async function () { it("changes to 'Failed' once too many slots are freed", async function () {
await waitUntilStarted(marketplace, request, proof) await waitUntilStarted(marketplace, request, proof, token)
await waitUntilFailed(marketplace, request) await waitUntilFailed(marketplace, request)
expect(await marketplace.requestState(slot.request)).to.equal(Failed) expect(await marketplace.requestState(slot.request)).to.equal(Failed)
}) })
it("does not change to 'Failed' before it is started", async function () { it("does not change to 'Failed' before it is started", async function () {
await token.approve(marketplace.address, request.ask.collateral * (request.ask.maxSlotLoss + 1))
for (let i = 0; i <= request.ask.maxSlotLoss; i++) { for (let i = 0; i <= request.ask.maxSlotLoss; i++) {
await marketplace.fillSlot(slot.request, i, proof) await marketplace.fillSlot(slot.request, i, proof)
} }
@ -551,13 +541,13 @@ describe("Marketplace", function () {
}) })
it("changes to 'Finished' when the request ends", async function () { it("changes to 'Finished' when the request ends", async function () {
await waitUntilStarted(marketplace, request, proof) await waitUntilStarted(marketplace, request, proof, token)
await waitUntilFinished(marketplace, requestId(request)) await waitUntilFinished(marketplace, requestId(request))
expect(await marketplace.requestState(slot.request)).to.equal(Finished) expect(await marketplace.requestState(slot.request)).to.equal(Finished)
}) })
it("remains 'Finished' once a slot is paid out", async function () { it("remains 'Finished' once a slot is paid out", async function () {
await waitUntilStarted(marketplace, request, proof) await waitUntilStarted(marketplace, request, proof, token)
await waitUntilFinished(marketplace, requestId(request)) await waitUntilFinished(marketplace, requestId(request))
await marketplace.freeSlot(slotId(slot)) await marketplace.freeSlot(slotId(slot))
expect(await marketplace.requestState(slot.request)).to.equal(Finished) expect(await marketplace.requestState(slot.request)).to.equal(Finished)
@ -576,8 +566,7 @@ describe("Marketplace", function () {
await token.approve(marketplace.address, price(request)) await token.approve(marketplace.address, price(request))
await marketplace.requestStorage(request) await marketplace.requestStorage(request)
switchAccount(host) switchAccount(host)
await token.approve(marketplace.address, config.collateral.initialAmount) await token.approve(marketplace.address, request.ask.collateral)
await marketplace.deposit(config.collateral.initialAmount)
}) })
async function waitUntilProofIsRequired(id) { async function waitUntilProofIsRequired(id) {
@ -602,7 +591,7 @@ describe("Marketplace", function () {
}) })
it("changes to 'Finished' when request finishes", async function () { it("changes to 'Finished' when request finishes", async function () {
await waitUntilStarted(marketplace, request, proof) await waitUntilStarted(marketplace, request, proof, token)
await waitUntilFinished(marketplace, slot.request) await waitUntilFinished(marketplace, slot.request)
expect(await marketplace.slotState(slotId(slot))).to.equal(Finished) expect(await marketplace.slotState(slotId(slot))).to.equal(Finished)
}) })
@ -620,7 +609,7 @@ describe("Marketplace", function () {
}) })
it("changes to 'Free' when too many proofs are missed", async function () { it("changes to 'Free' when too many proofs are missed", async function () {
await waitUntilStarted(marketplace, request, proof) await waitUntilStarted(marketplace, request, proof, token)
while ((await marketplace.slotState(slotId(slot))) === Filled) { while ((await marketplace.slotState(slotId(slot))) === Filled) {
await waitUntilProofIsRequired(slotId(slot)) await waitUntilProofIsRequired(slotId(slot))
const missedPeriod = periodOf(await currentTime()) const missedPeriod = periodOf(await currentTime())
@ -631,13 +620,13 @@ describe("Marketplace", function () {
}) })
it("changes to 'Failed' when request fails", async function () { it("changes to 'Failed' when request fails", async function () {
await waitUntilStarted(marketplace, request, proof) await waitUntilStarted(marketplace, request, proof, token)
await waitUntilSlotFailed(marketplace, request, slot) await waitUntilSlotFailed(marketplace, request, slot)
expect(await marketplace.slotState(slotId(slot))).to.equal(Failed) expect(await marketplace.slotState(slotId(slot))).to.equal(Failed)
}) })
it("changes to 'Paid' when host has been paid", async function () { it("changes to 'Paid' when host has been paid", async function () {
await waitUntilStarted(marketplace, request, proof) await waitUntilStarted(marketplace, request, proof, token)
await waitUntilFinished(marketplace, slot.request) await waitUntilFinished(marketplace, slot.request)
await marketplace.freeSlot(slotId(slot)) await marketplace.freeSlot(slotId(slot))
expect(await marketplace.slotState(slotId(slot))).to.equal(Paid) expect(await marketplace.slotState(slotId(slot))).to.equal(Paid)
@ -655,8 +644,7 @@ describe("Marketplace", function () {
await token.approve(marketplace.address, price(request)) await token.approve(marketplace.address, price(request))
await marketplace.requestStorage(request) await marketplace.requestStorage(request)
switchAccount(host) switchAccount(host)
await token.approve(marketplace.address, config.collateral.initialAmount) await token.approve(marketplace.address, request.ask.collateral)
await marketplace.deposit(config.collateral.initialAmount)
}) })
async function waitUntilProofWillBeRequired(id) { async function waitUntilProofWillBeRequired(id) {
@ -735,8 +723,7 @@ describe("Marketplace", function () {
await token.approve(marketplace.address, price(request)) await token.approve(marketplace.address, price(request))
await marketplace.requestStorage(request) await marketplace.requestStorage(request)
switchAccount(host) switchAccount(host)
await token.approve(marketplace.address, config.collateral.initialAmount) await token.approve(marketplace.address, request.ask.collateral)
await marketplace.deposit(config.collateral.initialAmount)
}) })
async function waitUntilProofIsRequired(id) { async function waitUntilProofIsRequired(id) {
@ -763,8 +750,7 @@ describe("Marketplace", function () {
describe("slashing when missing proofs", function () { describe("slashing when missing proofs", function () {
it("reduces collateral when too many proofs are missing", async function () { it("reduces collateral when too many proofs are missing", async function () {
const id = slotId(slot) const id = slotId(slot)
const { slashCriterion, slashPercentage, initialAmount } = const { slashCriterion, slashPercentage } = config.collateral
config.collateral
await marketplace.fillSlot(slot.request, slot.index, proof) await marketplace.fillSlot(slot.request, slot.index, proof)
for (let i = 0; i < slashCriterion; i++) { for (let i = 0; i < slashCriterion; i++) {
await waitUntilProofIsRequired(id) await waitUntilProofIsRequired(id)
@ -772,33 +758,31 @@ describe("Marketplace", function () {
await advanceTime(period) await advanceTime(period)
await marketplace.markProofAsMissing(id, missedPeriod) await marketplace.markProofAsMissing(id, missedPeriod)
} }
const expectedBalance = (initialAmount * (100 - slashPercentage)) / 100 const expectedBalance = (request.ask.collateral * (100 - slashPercentage)) / 100
expect(await marketplace.balanceOf(host.address)).to.equal(
expectedBalance expect(BigNumber.from(expectedBalance).eq(await marketplace.getSlotCollateral(id)))
)
}) })
}) })
it("frees slot when collateral slashed below minimum threshold", async function () { it("frees slot when collateral slashed below minimum threshold", async function () {
const minimum = config.collateral.minimumAmount const minimum = config.collateral.minimumAmount
await waitUntilStarted(marketplace, request, proof) await waitUntilStarted(marketplace, request, proof, token)
while ((await marketplace.slotState(slotId(slot))) === SlotState.Filled) { while ((await marketplace.slotState(slotId(slot))) === SlotState.Filled) {
expect(await marketplace.balanceOf(host.address)).to.be.gt(minimum) expect(await marketplace.getSlotCollateral(slotId(slot))).to.be.gt(minimum)
await waitUntilProofIsRequired(slotId(slot)) await waitUntilProofIsRequired(slotId(slot))
const missedPeriod = periodOf(await currentTime()) const missedPeriod = periodOf(await currentTime())
await advanceTime(period) await advanceTime(period)
await marketplace.markProofAsMissing(slotId(slot), missedPeriod) await marketplace.markProofAsMissing(slotId(slot), missedPeriod)
} }
expect(await marketplace.slotState(slotId(slot))).to.equal(SlotState.Free) expect(await marketplace.slotState(slotId(slot))).to.equal(SlotState.Free)
expect(await marketplace.balanceOf(host.address)).to.be.lte(minimum) expect(await marketplace.getSlotCollateral(slotId(slot))).to.be.lte(minimum)
}) })
}) })
describe("list of active requests", function () { describe("list of active requests", function () {
beforeEach(async function () { beforeEach(async function () {
switchAccount(host) switchAccount(host)
await token.approve(marketplace.address, config.collateral.initialAmount) await token.approve(marketplace.address, request.ask.collateral)
await marketplace.deposit(config.collateral.initialAmount)
switchAccount(client) switchAccount(client)
await token.approve(marketplace.address, price(request)) await token.approve(marketplace.address, price(request))
}) })
@ -824,7 +808,7 @@ describe("Marketplace", function () {
it("keeps request in list when request fails", async function () { it("keeps request in list when request fails", async function () {
await marketplace.requestStorage(request) await marketplace.requestStorage(request)
switchAccount(host) switchAccount(host)
await waitUntilStarted(marketplace, request, proof) await waitUntilStarted(marketplace, request, proof, token)
await waitUntilFailed(marketplace, request) await waitUntilFailed(marketplace, request)
switchAccount(client) switchAccount(client)
expect(await marketplace.myRequests()).to.deep.equal([requestId(request)]) expect(await marketplace.myRequests()).to.deep.equal([requestId(request)])
@ -833,7 +817,7 @@ describe("Marketplace", function () {
it("removes request from list when request finishes", async function () { it("removes request from list when request finishes", async function () {
await marketplace.requestStorage(request) await marketplace.requestStorage(request)
switchAccount(host) switchAccount(host)
await waitUntilStarted(marketplace, request, proof) await waitUntilStarted(marketplace, request, proof, token)
await waitUntilFinished(marketplace, requestId(request)) await waitUntilFinished(marketplace, requestId(request))
await marketplace.freeSlot(slotId(slot)) await marketplace.freeSlot(slotId(slot))
switchAccount(client) switchAccount(client)
@ -847,13 +831,13 @@ describe("Marketplace", function () {
await token.approve(marketplace.address, price(request)) await token.approve(marketplace.address, price(request))
await marketplace.requestStorage(request) await marketplace.requestStorage(request)
switchAccount(host) switchAccount(host)
await token.approve(marketplace.address, config.collateral.initialAmount) await token.approve(marketplace.address, request.ask.collateral)
await marketplace.deposit(config.collateral.initialAmount)
}) })
it("adds slot to list when filling slot", async function () { it("adds slot to list when filling slot", async function () {
await marketplace.fillSlot(slot.request, slot.index, proof) await marketplace.fillSlot(slot.request, slot.index, proof)
let slot1 = { ...slot, index: slot.index + 1 } let slot1 = { ...slot, index: slot.index + 1 }
await token.approve(marketplace.address, request.ask.collateral)
await marketplace.fillSlot(slot.request, slot1.index, proof) await marketplace.fillSlot(slot.request, slot1.index, proof)
expect(await marketplace.mySlots()).to.have.members([ expect(await marketplace.mySlots()).to.have.members([
slotId(slot), slotId(slot),
@ -864,7 +848,9 @@ describe("Marketplace", function () {
it("removes slot from list when slot is freed", async function () { it("removes slot from list when slot is freed", async function () {
await marketplace.fillSlot(slot.request, slot.index, proof) await marketplace.fillSlot(slot.request, slot.index, proof)
let slot1 = { ...slot, index: slot.index + 1 } let slot1 = { ...slot, index: slot.index + 1 }
await token.approve(marketplace.address, request.ask.collateral)
await marketplace.fillSlot(slot.request, slot1.index, proof) await marketplace.fillSlot(slot.request, slot1.index, proof)
await token.approve(marketplace.address, request.ask.collateral)
await marketplace.freeSlot(slotId(slot)) await marketplace.freeSlot(slotId(slot))
expect(await marketplace.mySlots()).to.have.members([slotId(slot1)]) expect(await marketplace.mySlots()).to.have.members([slotId(slot1)])
}) })
@ -872,6 +858,8 @@ describe("Marketplace", function () {
it("keeps slots when cancelled", async function () { it("keeps slots when cancelled", async function () {
await marketplace.fillSlot(slot.request, slot.index, proof) await marketplace.fillSlot(slot.request, slot.index, proof)
let slot1 = { ...slot, index: slot.index + 1 } let slot1 = { ...slot, index: slot.index + 1 }
await token.approve(marketplace.address, request.ask.collateral)
await marketplace.fillSlot(slot.request, slot1.index, proof) await marketplace.fillSlot(slot.request, slot1.index, proof)
await waitUntilCancelled(request) await waitUntilCancelled(request)
expect(await marketplace.mySlots()).to.have.members([ expect(await marketplace.mySlots()).to.have.members([
@ -881,7 +869,7 @@ describe("Marketplace", function () {
}) })
it("removes slot when finished slot is freed", async function () { it("removes slot when finished slot is freed", async function () {
await waitUntilStarted(marketplace, request, proof) await waitUntilStarted(marketplace, request, proof, token)
await waitUntilFinished(marketplace, requestId(request)) await waitUntilFinished(marketplace, requestId(request))
await marketplace.freeSlot(slotId(slot)) await marketplace.freeSlot(slotId(slot))
expect(await marketplace.mySlots()).to.not.contain(slotId(slot)) expect(await marketplace.mySlots()).to.not.contain(slotId(slot))
@ -895,7 +883,7 @@ describe("Marketplace", function () {
}) })
it("removes slot when failed slot is freed", async function () { it("removes slot when failed slot is freed", async function () {
await waitUntilStarted(marketplace, request, proof) await waitUntilStarted(marketplace, request, proof, token)
await waitUntilSlotFailed(marketplace, request, slot) await waitUntilSlotFailed(marketplace, request, slot)
await marketplace.freeSlot(slotId(slot)) await marketplace.freeSlot(slotId(slot))
expect(await marketplace.mySlots()).to.not.contain(slotId(slot)) expect(await marketplace.mySlots()).to.not.contain(slotId(slot))

View File

@ -5,7 +5,6 @@ const { hexlify, randomBytes } = ethers.utils
const exampleConfiguration = () => ({ const exampleConfiguration = () => ({
collateral: { collateral: {
initialAmount: 100,
minimumAmount: 40, minimumAmount: 40,
slashCriterion: 3, slashCriterion: 3,
slashPercentage: 10, slashPercentage: 10,
@ -28,6 +27,7 @@ const exampleRequest = async () => {
proofProbability: 4, // require a proof roughly once every 4 periods proofProbability: 4, // require a proof roughly once every 4 periods
reward: 84, reward: 84,
maxSlotLoss: 2, maxSlotLoss: 2,
collateral: 200,
}, },
content: { content: {
cid: "zb2rhheVmk3bLks5MgzTqyznLu1zqGH5jrfTA1eAZXrjx7Vob", cid: "zb2rhheVmk3bLks5MgzTqyznLu1zqGH5jrfTA1eAZXrjx7Vob",

View File

@ -2,7 +2,7 @@ const { ethers } = require("hardhat")
const { keccak256, defaultAbiCoder } = ethers.utils const { keccak256, defaultAbiCoder } = ethers.utils
function requestId(request) { function requestId(request) {
const Ask = "tuple(int64, uint256, uint256, uint256, uint256, int64)" const Ask = "tuple(int64, uint256, uint256, uint256, uint256, uint256, int64)"
const Erasure = "tuple(uint64)" const Erasure = "tuple(uint64)"
const PoR = "tuple(bytes, bytes, bytes)" const PoR = "tuple(bytes, bytes, bytes)"
const Content = "tuple(string, " + Erasure + ", " + PoR + ")" const Content = "tuple(string, " + Erasure + ", " + PoR + ")"
@ -18,6 +18,7 @@ function askToArray(ask) {
ask.duration, ask.duration,
ask.proofProbability, ask.proofProbability,
ask.reward, ask.reward,
ask.collateral,
ask.maxSlotLoss, ask.maxSlotLoss,
] ]
} }

View File

@ -1,11 +1,14 @@
const { advanceTimeTo } = require("./evm") const { advanceTimeTo } = require("./evm")
const { slotId, requestId } = require("./ids") const { slotId, requestId } = require("./ids")
const {price} = require("./price");
async function waitUntilCancelled(request) { async function waitUntilCancelled(request) {
await advanceTimeTo(request.expiry + 1) await advanceTimeTo(request.expiry + 1)
} }
async function waitUntilStarted(contract, request, proof) { async function waitUntilStarted(contract, request, proof, token) {
await token.approve(contract.address, price(request)*request.ask.slots)
for (let i = 0; i < request.ask.slots; i++) { for (let i = 0; i < request.ask.slots; i++) {
await contract.fillSlot(requestId(request), i, proof) await contract.fillSlot(requestId(request), i, proof)
} }