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 {
uint256 initialAmount; // amount of collateral necessary to fill a slot
uint256 minimumAmount; // frees slot when collateral drops below this minimum
uint256 slashCriterion; // amount of proofs missed that lead to slashing
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 "./Configuration.sol";
import "./Requests.sol";
import "./Collateral.sol";
import "./Proofs.sol";
import "./StateRetrieval.sol";
contract Marketplace is Collateral, Proofs, StateRetrieval {
contract Marketplace is Proofs, StateRetrieval {
using EnumerableSet for EnumerableSet.Bytes32Set;
using Requests for Request;
IERC20 public immutable token;
MarketplaceConfig public config;
MarketplaceFunds private _funds;
mapping(RequestId => Request) private _requests;
mapping(RequestId => RequestContext) private _requestContexts;
mapping(SlotId => Slot) private _slots;
mapping(SlotId => Slot) internal _slots;
struct RequestContext {
RequestState state;
@ -31,20 +31,22 @@ contract Marketplace is Collateral, Proofs, StateRetrieval {
struct Slot {
SlotState state;
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;
}
constructor(
IERC20 token,
IERC20 token_,
MarketplaceConfig memory configuration
) Collateral(token) Proofs(configuration.proofs) marketplaceInvariant {
) Proofs(configuration.proofs) marketplaceInvariant {
token = token_;
config = configuration;
}
function _isWithdrawAllowed() internal view override returns (bool) {
return !_hasSlots(msg.sender);
}
function requestStorage(
Request calldata request
) public marketplaceInvariant {
@ -80,11 +82,6 @@ contract Marketplace is Collateral, Proofs, StateRetrieval {
require(slotState(slotId) == SlotState.Free, "Slot is not free");
require(
balanceOf(msg.sender) >= config.collateral.initialAmount,
"Insufficient collateral"
);
_startRequiringProofs(slotId, request.ask.proofProbability);
submitProof(slotId, proof);
@ -93,6 +90,13 @@ contract Marketplace is Collateral, Proofs, StateRetrieval {
RequestContext storage context = _requestContexts[requestId];
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);
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");
SlotState state = slotState(slotId);
require(state != SlotState.Paid, "Already paid");
if (state == SlotState.Finished) {
_payoutSlot(slot.requestId, slotId);
} else if (state == SlotState.Failed) {
@ -120,11 +125,15 @@ contract Marketplace is Collateral, Proofs, StateRetrieval {
function markProofAsMissing(SlotId slotId, Period period) public {
require(slotState(slotId) == SlotState.Filled, "Slot not accepting proofs");
_markProofAsMissing(slotId, period);
address host = getHost(slotId);
if (missingProofs(slotId) % config.collateral.slashCriterion == 0) {
_slash(host, config.collateral.slashPercentage);
Slot storage slot = _slots[slotId];
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
// needs to be freed so that there is enough remaining collateral to be
// 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;
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);
slot.state = SlotState.Free;
slot.host = address(0);
slot.requestId = RequestId.wrap(0);
delete _slots[slotId];
context.slotsFilled -= 1;
emit SlotFreed(requestId, slotId);
@ -161,8 +163,6 @@ contract Marketplace is Collateral, Proofs, StateRetrieval {
context.endsAt = block.timestamp - 1;
emit RequestFailed(requestId);
// TODO: burn all remaining slot collateral (note: slot collateral not
// yet implemented)
// TODO: send client remaining funds
}
}
@ -179,7 +179,7 @@ contract Marketplace is Collateral, Proofs, StateRetrieval {
_removeFromMySlots(slot.host, slotId);
uint256 amount = _requests[requestId].pricePerSlot();
uint256 amount = _requests[requestId].pricePerSlot() + slot.currentCollateral;
_funds.sent += amount;
_funds.balance -= amount;
slot.state = SlotState.Paid;
@ -212,10 +212,6 @@ contract Marketplace is Collateral, Proofs, StateRetrieval {
require(token.transfer(msg.sender, amount), "Withdraw failed");
}
function getHost(SlotId slotId) public view returns (address) {
return _slots[slotId].host;
}
function getRequestFromSlotId(SlotId slotId)
public
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(
RequestId requestId
) public view requestIsKnown(requestId) returns (RequestState) {
@ -291,6 +291,11 @@ contract Marketplace is Collateral, Proofs, StateRetrieval {
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 RequestFulfilled(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.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 {
uint256 balance;
uint256 received;
uint256 sent;
uint256 slashed;
}
}

View File

@ -8,7 +8,7 @@ struct Request {
address client;
Ask ask;
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
}
@ -18,6 +18,7 @@ struct Ask {
uint256 duration; // how long content should be stored (in seconds)
uint256 proofProbability; // how often storage proofs are required
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
}

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 {
_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 configuration = {
collateral: {
initialAmount: 100,
minimumAmount: 40,
slashCriterion: 3,
slashPercentage: 10,

44
package-lock.json generated
View File

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

View File

@ -15,8 +15,8 @@
"chai": "^4.3.7",
"ethereum-waffle": "^3.4.4",
"ethers": "^5.7.2",
"hardhat": "^2.12.5",
"hardhat-deploy": "^0.11.22",
"hardhat": "^2.12.7",
"hardhat-deploy": "^0.11.23",
"hardhat-deploy-ethers": "^0.3.0-beta.13",
"prettier": "^2.8.2",
"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 () {
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)
await token.approve(marketplace.address, request.ask.collateral)
})
it("emits event when slot is filled", async function () {
@ -160,16 +159,6 @@ describe("Marketplace", function () {
).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 () {
await marketplace.fillSlot(slot.request, slot.index, proof)
await expect(
@ -196,15 +185,15 @@ describe("Marketplace", function () {
})
it("is rejected when request is finished", async function () {
await waitUntilStarted(marketplace, request, proof)
await waitUntilFinished(marketplace, requestId(request))
await waitUntilStarted(marketplace, request, proof, token)
await waitUntilFinished(marketplace, slot.request)
await expect(
marketplace.fillSlot(slot.request, slot.index, proof)
).to.be.revertedWith("Slot is not free")
})
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 expect(
marketplace.fillSlot(slot.request, slot.index, proof)
@ -220,6 +209,8 @@ describe("Marketplace", function () {
it("fails when all slots are already filled", async function () {
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++) {
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 () {
var requestTime
beforeEach(async function () {
@ -237,8 +253,7 @@ describe("Marketplace", function () {
await marketplace.requestStorage(request)
requestTime = await currentTime()
switchAccount(host)
await token.approve(marketplace.address, config.collateral.initialAmount)
await marketplace.deposit(config.collateral.initialAmount)
await token.approve(marketplace.address, request.ask.collateral)
})
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 () {
await waitUntilStarted(marketplace, request, proof)
await waitUntilStarted(marketplace, request, proof, token)
await waitUntilFailed(marketplace, request)
let slot0 = { ...slot, index: request.ask.maxSlotLoss + 1 }
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 () {
await waitUntilStarted(marketplace, request, proof)
await waitUntilStarted(marketplace, request, proof, token)
await waitUntilFinished(marketplace, requestId(request))
const now = await currentTime()
// in the process of calling currentTime and requestEnd,
@ -281,7 +296,8 @@ describe("Marketplace", function () {
})
describe("freeing a slot", function () {
var id
let id
beforeEach(async function () {
slot.index = 0
id = slotId(slot)
@ -290,8 +306,7 @@ describe("Marketplace", function () {
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)
await token.approve(marketplace.address, request.ask.collateral)
})
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 () {
await waitUntilStarted(marketplace, request, proof)
await waitUntilStarted(marketplace, request, proof, token)
switchAccount(client)
await expect(marketplace.freeSlot(id)).to.be.revertedWith(
"Slot filled by other host"
@ -311,12 +326,12 @@ describe("Marketplace", 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
})
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))
.to.emit(marketplace, "SlotFreed")
.withArgs(slot.request, id)
@ -329,17 +344,16 @@ describe("Marketplace", function () {
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)
await token.approve(marketplace.address, request.ask.collateral)
})
it("pays the host when contract has finished", async function () {
await waitUntilStarted(marketplace, request, proof)
it("pays the host when contract has finished and returns collateral", async function () {
await waitUntilStarted(marketplace, request, proof, token)
await waitUntilFinished(marketplace, requestId(request))
const startBalance = await token.balanceOf(host.address)
await marketplace.freeSlot(slotId(slot))
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 () {
@ -360,7 +374,7 @@ describe("Marketplace", 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 marketplace.freeSlot(slotId(slot))
await expect(marketplace.freeSlot(slotId(slot))).to.be.revertedWith(
@ -369,7 +383,7 @@ describe("Marketplace", 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 marketplace.freeSlot(slotId(slot))
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 marketplace.requestStorage(request)
switchAccount(host)
await token.approve(marketplace.address, config.collateral.initialAmount)
await marketplace.deposit(config.collateral.initialAmount)
await token.approve(marketplace.address, request.ask.collateral)
})
it("emits event when all slots are filled", async function () {
const lastSlot = request.ask.slots - 1
await token.approve(marketplace.address, request.ask.collateral * lastSlot)
for (let i = 0; i < lastSlot; i++) {
await marketplace.fillSlot(slot.request, i, proof)
}
await token.approve(marketplace.address, request.ask.collateral)
await expect(marketplace.fillSlot(slot.request, lastSlot, proof))
.to.emit(marketplace, "RequestFulfilled")
.withArgs(requestId(request))
})
it("sets state when all slots are filled", async function () {
const lastSlot = request.ask.slots - 1
for (let i = 0; i <= lastSlot; i++) {
const slots = request.ask.slots
await token.approve(marketplace.address, request.ask.collateral * slots)
for (let i = 0; i < slots; i++) {
await marketplace.fillSlot(slot.request, i, proof)
}
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 () {
const lastSlot = request.ask.slots - 1
await token.approve(marketplace.address, request.ask.collateral * (lastSlot + 1))
for (let i = 0; i <= lastSlot; i++) {
await marketplace.fillSlot(slot.request, i, proof)
}
@ -422,8 +440,7 @@ describe("Marketplace", function () {
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)
await token.approve(marketplace.address, request.ask.collateral)
})
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 () {
// fill all slots, should change state to RequestState.Started
const lastSlot = request.ask.slots - 1
await token.approve(marketplace.address, request.ask.collateral * (lastSlot + 1))
for (let i = 0; i <= lastSlot; i++) {
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 () {
const { New, Cancelled, Started, Failed, Finished } = RequestState
@ -507,8 +497,7 @@ describe("Marketplace", function () {
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)
await token.approve(marketplace.address, request.ask.collateral)
})
it("is 'New' initially", async function () {
@ -528,17 +517,18 @@ describe("Marketplace", 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)
})
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)
expect(await marketplace.requestState(slot.request)).to.equal(Failed)
})
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++) {
await marketplace.fillSlot(slot.request, i, proof)
}
@ -551,13 +541,13 @@ describe("Marketplace", 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))
expect(await marketplace.requestState(slot.request)).to.equal(Finished)
})
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 marketplace.freeSlot(slotId(slot))
expect(await marketplace.requestState(slot.request)).to.equal(Finished)
@ -576,8 +566,7 @@ describe("Marketplace", function () {
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)
await token.approve(marketplace.address, request.ask.collateral)
})
async function waitUntilProofIsRequired(id) {
@ -602,7 +591,7 @@ describe("Marketplace", 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)
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 () {
await waitUntilStarted(marketplace, request, proof)
await waitUntilStarted(marketplace, request, proof, token)
while ((await marketplace.slotState(slotId(slot))) === Filled) {
await waitUntilProofIsRequired(slotId(slot))
const missedPeriod = periodOf(await currentTime())
@ -631,13 +620,13 @@ describe("Marketplace", 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)
expect(await marketplace.slotState(slotId(slot))).to.equal(Failed)
})
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 marketplace.freeSlot(slotId(slot))
expect(await marketplace.slotState(slotId(slot))).to.equal(Paid)
@ -655,8 +644,7 @@ describe("Marketplace", function () {
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)
await token.approve(marketplace.address, request.ask.collateral)
})
async function waitUntilProofWillBeRequired(id) {
@ -735,8 +723,7 @@ describe("Marketplace", function () {
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)
await token.approve(marketplace.address, request.ask.collateral)
})
async function waitUntilProofIsRequired(id) {
@ -763,8 +750,7 @@ describe("Marketplace", function () {
describe("slashing when missing proofs", function () {
it("reduces collateral when too many proofs are missing", async function () {
const id = slotId(slot)
const { slashCriterion, slashPercentage, initialAmount } =
config.collateral
const { slashCriterion, slashPercentage } = config.collateral
await marketplace.fillSlot(slot.request, slot.index, proof)
for (let i = 0; i < slashCriterion; i++) {
await waitUntilProofIsRequired(id)
@ -772,33 +758,31 @@ describe("Marketplace", function () {
await advanceTime(period)
await marketplace.markProofAsMissing(id, missedPeriod)
}
const expectedBalance = (initialAmount * (100 - slashPercentage)) / 100
expect(await marketplace.balanceOf(host.address)).to.equal(
expectedBalance
)
const expectedBalance = (request.ask.collateral * (100 - slashPercentage)) / 100
expect(BigNumber.from(expectedBalance).eq(await marketplace.getSlotCollateral(id)))
})
})
it("frees slot when collateral slashed below minimum threshold", async function () {
const minimum = config.collateral.minimumAmount
await waitUntilStarted(marketplace, request, proof)
await waitUntilStarted(marketplace, request, proof, token)
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))
const missedPeriod = periodOf(await currentTime())
await advanceTime(period)
await marketplace.markProofAsMissing(slotId(slot), missedPeriod)
}
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 () {
beforeEach(async function () {
switchAccount(host)
await token.approve(marketplace.address, config.collateral.initialAmount)
await marketplace.deposit(config.collateral.initialAmount)
await token.approve(marketplace.address, request.ask.collateral)
switchAccount(client)
await token.approve(marketplace.address, price(request))
})
@ -824,7 +808,7 @@ describe("Marketplace", function () {
it("keeps request in list when request fails", async function () {
await marketplace.requestStorage(request)
switchAccount(host)
await waitUntilStarted(marketplace, request, proof)
await waitUntilStarted(marketplace, request, proof, token)
await waitUntilFailed(marketplace, request)
switchAccount(client)
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 () {
await marketplace.requestStorage(request)
switchAccount(host)
await waitUntilStarted(marketplace, request, proof)
await waitUntilStarted(marketplace, request, proof, token)
await waitUntilFinished(marketplace, requestId(request))
await marketplace.freeSlot(slotId(slot))
switchAccount(client)
@ -847,13 +831,13 @@ describe("Marketplace", function () {
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)
await token.approve(marketplace.address, request.ask.collateral)
})
it("adds slot to list when filling slot", async function () {
await marketplace.fillSlot(slot.request, slot.index, proof)
let slot1 = { ...slot, index: slot.index + 1 }
await token.approve(marketplace.address, request.ask.collateral)
await marketplace.fillSlot(slot.request, slot1.index, proof)
expect(await marketplace.mySlots()).to.have.members([
slotId(slot),
@ -864,7 +848,9 @@ describe("Marketplace", function () {
it("removes slot from list when slot is freed", async function () {
await marketplace.fillSlot(slot.request, slot.index, proof)
let slot1 = { ...slot, index: slot.index + 1 }
await token.approve(marketplace.address, request.ask.collateral)
await marketplace.fillSlot(slot.request, slot1.index, proof)
await token.approve(marketplace.address, request.ask.collateral)
await marketplace.freeSlot(slotId(slot))
expect(await marketplace.mySlots()).to.have.members([slotId(slot1)])
})
@ -872,6 +858,8 @@ describe("Marketplace", function () {
it("keeps slots when cancelled", async function () {
await marketplace.fillSlot(slot.request, slot.index, proof)
let slot1 = { ...slot, index: slot.index + 1 }
await token.approve(marketplace.address, request.ask.collateral)
await marketplace.fillSlot(slot.request, slot1.index, proof)
await waitUntilCancelled(request)
expect(await marketplace.mySlots()).to.have.members([
@ -881,7 +869,7 @@ describe("Marketplace", 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 marketplace.freeSlot(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 () {
await waitUntilStarted(marketplace, request, proof)
await waitUntilStarted(marketplace, request, proof, token)
await waitUntilSlotFailed(marketplace, request, slot)
await marketplace.freeSlot(slotId(slot))
expect(await marketplace.mySlots()).to.not.contain(slotId(slot))

View File

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

View File

@ -2,7 +2,7 @@ const { ethers } = require("hardhat")
const { keccak256, defaultAbiCoder } = ethers.utils
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 PoR = "tuple(bytes, bytes, bytes)"
const Content = "tuple(string, " + Erasure + ", " + PoR + ")"
@ -18,6 +18,7 @@ function askToArray(ask) {
ask.duration,
ask.proofProbability,
ask.reward,
ask.collateral,
ask.maxSlotLoss,
]
}

View File

@ -1,11 +1,14 @@
const { advanceTimeTo } = require("./evm")
const { slotId, requestId } = require("./ids")
const {price} = require("./price");
async function waitUntilCancelled(request) {
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++) {
await contract.fillSlot(requestId(request), i, proof)
}