mirror of
https://github.com/status-im/dagger-contracts.git
synced 2025-02-26 05:15:31 +00:00
Remove Contracts in favor of Marketplace
This commit is contained in:
parent
7e7134b99d
commit
e818d70b85
@ -1,132 +0,0 @@
|
|||||||
// SPDX-License-Identifier: MIT
|
|
||||||
pragma solidity ^0.8.0;
|
|
||||||
|
|
||||||
import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";
|
|
||||||
|
|
||||||
contract Contracts {
|
|
||||||
mapping(bytes32 => bool) private ids; // contract id, equal to hash of bid
|
|
||||||
mapping(bytes32 => uint256) private durations; // contract duration in blocks
|
|
||||||
mapping(bytes32 => uint256) private sizes; // storage size in bytes
|
|
||||||
mapping(bytes32 => bytes32) private contentHashes; // hash of data to be stored
|
|
||||||
mapping(bytes32 => uint256) private proofPeriods; // period between proofs
|
|
||||||
mapping(bytes32 => uint256) private proofTimeouts; // timeout for proof submission
|
|
||||||
mapping(bytes32 => uint256) private prices; // price in coins
|
|
||||||
mapping(bytes32 => address) private hosts; // host that provides storage
|
|
||||||
|
|
||||||
function _duration(bytes32 id) internal view returns (uint256) {
|
|
||||||
return durations[id];
|
|
||||||
}
|
|
||||||
|
|
||||||
function _size(bytes32 id) internal view returns (uint256) {
|
|
||||||
return sizes[id];
|
|
||||||
}
|
|
||||||
|
|
||||||
function _contentHash(bytes32 id) internal view returns (bytes32) {
|
|
||||||
return contentHashes[id];
|
|
||||||
}
|
|
||||||
|
|
||||||
function _proofPeriod(bytes32 id) internal view returns (uint256) {
|
|
||||||
return proofPeriods[id];
|
|
||||||
}
|
|
||||||
|
|
||||||
function _proofTimeout(bytes32 id) internal view returns (uint256) {
|
|
||||||
return proofTimeouts[id];
|
|
||||||
}
|
|
||||||
|
|
||||||
function _price(bytes32 id) internal view returns (uint256) {
|
|
||||||
return prices[id];
|
|
||||||
}
|
|
||||||
|
|
||||||
function _host(bytes32 id) internal view returns (address) {
|
|
||||||
return hosts[id];
|
|
||||||
}
|
|
||||||
|
|
||||||
function _newContract(
|
|
||||||
uint256 duration,
|
|
||||||
uint256 size,
|
|
||||||
bytes32 contentHash,
|
|
||||||
uint256 proofPeriod,
|
|
||||||
uint256 proofTimeout,
|
|
||||||
bytes32 nonce,
|
|
||||||
uint256 price,
|
|
||||||
address host,
|
|
||||||
uint256 bidExpiry,
|
|
||||||
bytes memory requestSignature,
|
|
||||||
bytes memory bidSignature
|
|
||||||
) internal returns (bytes32 id) {
|
|
||||||
bytes32 requestHash = _hashRequest(
|
|
||||||
duration,
|
|
||||||
size,
|
|
||||||
contentHash,
|
|
||||||
proofPeriod,
|
|
||||||
proofTimeout,
|
|
||||||
nonce
|
|
||||||
);
|
|
||||||
bytes32 bidHash = _hashBid(requestHash, bidExpiry, price);
|
|
||||||
_checkSignature(requestSignature, requestHash, msg.sender);
|
|
||||||
_checkSignature(bidSignature, bidHash, host);
|
|
||||||
_checkBidExpiry(bidExpiry);
|
|
||||||
_checkId(bidHash);
|
|
||||||
id = bidHash;
|
|
||||||
ids[id] = true;
|
|
||||||
durations[id] = duration;
|
|
||||||
sizes[id] = size;
|
|
||||||
contentHashes[id] = contentHash;
|
|
||||||
proofPeriods[id] = proofPeriod;
|
|
||||||
proofTimeouts[id] = proofTimeout;
|
|
||||||
prices[id] = price;
|
|
||||||
hosts[id] = host;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Creates hash for a storage request that can be used to check its signature.
|
|
||||||
function _hashRequest(
|
|
||||||
uint256 duration,
|
|
||||||
uint256 size,
|
|
||||||
bytes32 hash,
|
|
||||||
uint256 proofPeriod,
|
|
||||||
uint256 proofTimeout,
|
|
||||||
bytes32 nonce
|
|
||||||
) internal pure returns (bytes32) {
|
|
||||||
return
|
|
||||||
keccak256(
|
|
||||||
abi.encode(
|
|
||||||
"[dagger.request.v1]",
|
|
||||||
duration,
|
|
||||||
size,
|
|
||||||
hash,
|
|
||||||
proofPeriod,
|
|
||||||
proofTimeout,
|
|
||||||
nonce
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Creates hash for a storage bid that can be used to check its signature.
|
|
||||||
function _hashBid(
|
|
||||||
bytes32 requestHash,
|
|
||||||
uint256 expiry,
|
|
||||||
uint256 price
|
|
||||||
) internal pure returns (bytes32) {
|
|
||||||
return keccak256(abi.encode("[dagger.bid.v1]", requestHash, expiry, price));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Checks a signature for a storage request or bid, given its hash.
|
|
||||||
function _checkSignature(
|
|
||||||
bytes memory signature,
|
|
||||||
bytes32 hash,
|
|
||||||
address signer
|
|
||||||
) private pure {
|
|
||||||
bytes32 messageHash = ECDSA.toEthSignedMessageHash(hash);
|
|
||||||
address recovered = ECDSA.recover(messageHash, signature);
|
|
||||||
require(recovered == signer, "Invalid signature");
|
|
||||||
}
|
|
||||||
|
|
||||||
function _checkBidExpiry(uint256 expiry) private view {
|
|
||||||
// solhint-disable-next-line not-rely-on-time
|
|
||||||
require(expiry > block.timestamp, "Bid expired");
|
|
||||||
}
|
|
||||||
|
|
||||||
function _checkId(bytes32 id) private view {
|
|
||||||
require(!ids[id], "Contract already exists");
|
|
||||||
}
|
|
||||||
}
|
|
@ -85,6 +85,14 @@ contract Marketplace is Collateral {
|
|||||||
emit OfferSelected(id, offer.requestId);
|
emit OfferSelected(id, offer.requestId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function _request(bytes32 id) internal view returns (Request storage) {
|
||||||
|
return requests[id];
|
||||||
|
}
|
||||||
|
|
||||||
|
function _offer(bytes32 id) internal view returns (Offer storage) {
|
||||||
|
return offers[id];
|
||||||
|
}
|
||||||
|
|
||||||
struct Request {
|
struct Request {
|
||||||
address client;
|
address client;
|
||||||
uint256 duration;
|
uint256 duration;
|
||||||
|
@ -1,11 +1,11 @@
|
|||||||
// SPDX-License-Identifier: MIT
|
// SPDX-License-Identifier: MIT
|
||||||
pragma solidity ^0.8.0;
|
pragma solidity ^0.8.0;
|
||||||
|
|
||||||
import "./Contracts.sol";
|
import "./Marketplace.sol";
|
||||||
import "./Proofs.sol";
|
import "./Proofs.sol";
|
||||||
import "./Collateral.sol";
|
import "./Collateral.sol";
|
||||||
|
|
||||||
contract Storage is Contracts, Proofs, Collateral {
|
contract Storage is Collateral, Marketplace, Proofs {
|
||||||
uint256 public collateralAmount;
|
uint256 public collateralAmount;
|
||||||
uint256 public slashMisses;
|
uint256 public slashMisses;
|
||||||
uint256 public slashPercentage;
|
uint256 public slashPercentage;
|
||||||
@ -17,96 +17,30 @@ contract Storage is Contracts, Proofs, Collateral {
|
|||||||
uint256 _collateralAmount,
|
uint256 _collateralAmount,
|
||||||
uint256 _slashMisses,
|
uint256 _slashMisses,
|
||||||
uint256 _slashPercentage
|
uint256 _slashPercentage
|
||||||
) Collateral(token) {
|
) Marketplace(token, _collateralAmount) {
|
||||||
collateralAmount = _collateralAmount;
|
collateralAmount = _collateralAmount;
|
||||||
slashMisses = _slashMisses;
|
slashMisses = _slashMisses;
|
||||||
slashPercentage = _slashPercentage;
|
slashPercentage = _slashPercentage;
|
||||||
}
|
}
|
||||||
|
|
||||||
function newContract(
|
function startContract(bytes32 id) public {
|
||||||
uint256 _duration,
|
Offer storage offer = _offer(id);
|
||||||
uint256 _size,
|
require(msg.sender == offer.host, "Only host can call this function");
|
||||||
bytes32 _contentHash,
|
Request storage request = _request(offer.requestId);
|
||||||
uint256 _proofPeriod,
|
_expectProofs(
|
||||||
uint256 _proofTimeout,
|
id,
|
||||||
bytes32 _nonce,
|
request.proofPeriod,
|
||||||
uint256 _price,
|
request.proofTimeout,
|
||||||
address _host,
|
request.duration
|
||||||
uint256 _bidExpiry,
|
|
||||||
bytes memory requestSignature,
|
|
||||||
bytes memory bidSignature
|
|
||||||
) public {
|
|
||||||
require(balanceOf(_host) >= collateralAmount, "Insufficient collateral");
|
|
||||||
bytes32 requestHash = _hashRequest(
|
|
||||||
_duration,
|
|
||||||
_size,
|
|
||||||
_contentHash,
|
|
||||||
_proofPeriod,
|
|
||||||
_proofTimeout,
|
|
||||||
_nonce
|
|
||||||
);
|
);
|
||||||
bytes32 bidHash = _hashBid(requestHash, _bidExpiry, _price);
|
|
||||||
_createLock(bidHash, _bidExpiry);
|
|
||||||
_lock(_host, bidHash);
|
|
||||||
token.transferFrom(msg.sender, address(this), _price);
|
|
||||||
_newContract(
|
|
||||||
_duration,
|
|
||||||
_size,
|
|
||||||
_contentHash,
|
|
||||||
_proofPeriod,
|
|
||||||
_proofTimeout,
|
|
||||||
_nonce,
|
|
||||||
_price,
|
|
||||||
_host,
|
|
||||||
_bidExpiry,
|
|
||||||
requestSignature,
|
|
||||||
bidSignature
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
modifier onlyHost(bytes32 id) {
|
|
||||||
require(msg.sender == host(id), "Only host can call this function");
|
|
||||||
_;
|
|
||||||
}
|
|
||||||
|
|
||||||
function startContract(bytes32 id) public onlyHost(id) {
|
|
||||||
_expectProofs(id, proofPeriod(id), proofTimeout(id), duration(id));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function finishContract(bytes32 id) public {
|
function finishContract(bytes32 id) public {
|
||||||
require(block.number > proofEnd(id), "Contract has not ended yet");
|
require(block.number > proofEnd(id), "Contract has not ended yet");
|
||||||
require(!finished[id], "Contract already finished");
|
require(!finished[id], "Contract already finished");
|
||||||
_unlock(id);
|
|
||||||
finished[id] = true;
|
finished[id] = true;
|
||||||
require(token.transfer(host(id), price(id)), "Payment failed");
|
Offer storage offer = _offer(id);
|
||||||
}
|
require(token.transfer(offer.host, offer.price), "Payment failed");
|
||||||
|
|
||||||
function duration(bytes32 contractId) public view returns (uint256) {
|
|
||||||
return _duration(contractId);
|
|
||||||
}
|
|
||||||
|
|
||||||
function size(bytes32 contractId) public view returns (uint256) {
|
|
||||||
return _size(contractId);
|
|
||||||
}
|
|
||||||
|
|
||||||
function contentHash(bytes32 contractId) public view returns (bytes32) {
|
|
||||||
return _contentHash(contractId);
|
|
||||||
}
|
|
||||||
|
|
||||||
function price(bytes32 contractId) public view returns (uint256) {
|
|
||||||
return _price(contractId);
|
|
||||||
}
|
|
||||||
|
|
||||||
function host(bytes32 contractId) public view returns (address) {
|
|
||||||
return _host(contractId);
|
|
||||||
}
|
|
||||||
|
|
||||||
function proofPeriod(bytes32 contractId) public view returns (uint256) {
|
|
||||||
return _proofPeriod(contractId);
|
|
||||||
}
|
|
||||||
|
|
||||||
function proofTimeout(bytes32 contractId) public view returns (uint256) {
|
|
||||||
return _proofTimeout(contractId);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function proofEnd(bytes32 contractId) public view returns (uint256) {
|
function proofEnd(bytes32 contractId) public view returns (uint256) {
|
||||||
@ -144,7 +78,8 @@ contract Storage is Contracts, Proofs, Collateral {
|
|||||||
function markProofAsMissing(bytes32 contractId, uint256 blocknumber) public {
|
function markProofAsMissing(bytes32 contractId, uint256 blocknumber) public {
|
||||||
_markProofAsMissing(contractId, blocknumber);
|
_markProofAsMissing(contractId, blocknumber);
|
||||||
if (_missed(contractId) % slashMisses == 0) {
|
if (_missed(contractId) % slashMisses == 0) {
|
||||||
_slash(host(contractId), slashPercentage);
|
Offer storage offer = _offer(contractId);
|
||||||
|
_slash(offer.host, slashPercentage);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,55 +0,0 @@
|
|||||||
// SPDX-License-Identifier: MIT
|
|
||||||
pragma solidity ^0.8.0;
|
|
||||||
|
|
||||||
import "./Contracts.sol";
|
|
||||||
|
|
||||||
// exposes internal functions of Contracts for testing
|
|
||||||
contract TestContracts is Contracts {
|
|
||||||
function newContract(
|
|
||||||
uint256 _duration,
|
|
||||||
uint256 _size,
|
|
||||||
bytes32 _contentHash,
|
|
||||||
uint256 _proofPeriod,
|
|
||||||
uint256 _proofTimeout,
|
|
||||||
bytes32 _nonce,
|
|
||||||
uint256 _price,
|
|
||||||
address _host,
|
|
||||||
uint256 _bidExpiry,
|
|
||||||
bytes memory requestSignature,
|
|
||||||
bytes memory bidSignature
|
|
||||||
) public {
|
|
||||||
_newContract(
|
|
||||||
_duration,
|
|
||||||
_size,
|
|
||||||
_contentHash,
|
|
||||||
_proofPeriod,
|
|
||||||
_proofTimeout,
|
|
||||||
_nonce,
|
|
||||||
_price,
|
|
||||||
_host,
|
|
||||||
_bidExpiry,
|
|
||||||
requestSignature,
|
|
||||||
bidSignature
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function duration(bytes32 id) public view returns (uint256) {
|
|
||||||
return _duration(id);
|
|
||||||
}
|
|
||||||
|
|
||||||
function size(bytes32 id) public view returns (uint256) {
|
|
||||||
return _size(id);
|
|
||||||
}
|
|
||||||
|
|
||||||
function contentHash(bytes32 id) public view returns (bytes32) {
|
|
||||||
return _contentHash(id);
|
|
||||||
}
|
|
||||||
|
|
||||||
function price(bytes32 id) public view returns (uint256) {
|
|
||||||
return _price(id);
|
|
||||||
}
|
|
||||||
|
|
||||||
function host(bytes32 id) public view returns (address) {
|
|
||||||
return _host(id);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,138 +0,0 @@
|
|||||||
const { expect } = require("chai")
|
|
||||||
const { ethers } = require("hardhat")
|
|
||||||
const { hashRequest, hashBid, sign } = require("./marketplace")
|
|
||||||
const { exampleRequest, exampleBid } = require("./examples")
|
|
||||||
|
|
||||||
describe("Contracts", function () {
|
|
||||||
const request = exampleRequest()
|
|
||||||
const bid = exampleBid()
|
|
||||||
|
|
||||||
let client, host
|
|
||||||
let contracts
|
|
||||||
let requestHash, bidHash
|
|
||||||
let id
|
|
||||||
|
|
||||||
beforeEach(async function () {
|
|
||||||
;[client, host] = await ethers.getSigners()
|
|
||||||
let Contracts = await ethers.getContractFactory("TestContracts")
|
|
||||||
contracts = await Contracts.deploy()
|
|
||||||
requestHash = hashRequest(request)
|
|
||||||
bidHash = hashBid({ ...bid, requestHash })
|
|
||||||
id = bidHash
|
|
||||||
})
|
|
||||||
|
|
||||||
it("creates a new storage contract", async function () {
|
|
||||||
await contracts.newContract(
|
|
||||||
request.duration,
|
|
||||||
request.size,
|
|
||||||
request.contentHash,
|
|
||||||
request.proofPeriod,
|
|
||||||
request.proofTimeout,
|
|
||||||
request.nonce,
|
|
||||||
bid.price,
|
|
||||||
await host.getAddress(),
|
|
||||||
bid.bidExpiry,
|
|
||||||
await sign(client, requestHash),
|
|
||||||
await sign(host, bidHash)
|
|
||||||
)
|
|
||||||
expect(await contracts.duration(id)).to.equal(request.duration)
|
|
||||||
expect(await contracts.size(id)).to.equal(request.size)
|
|
||||||
expect(await contracts.contentHash(id)).to.equal(request.contentHash)
|
|
||||||
expect(await contracts.price(id)).to.equal(bid.price)
|
|
||||||
expect(await contracts.host(id)).to.equal(await host.getAddress())
|
|
||||||
})
|
|
||||||
|
|
||||||
it("does not allow reuse of contract ids", async function () {
|
|
||||||
await contracts.newContract(
|
|
||||||
request.duration,
|
|
||||||
request.size,
|
|
||||||
request.contentHash,
|
|
||||||
request.proofPeriod,
|
|
||||||
request.proofTimeout,
|
|
||||||
request.nonce,
|
|
||||||
bid.price,
|
|
||||||
await host.getAddress(),
|
|
||||||
bid.bidExpiry,
|
|
||||||
await sign(client, requestHash),
|
|
||||||
await sign(host, bidHash)
|
|
||||||
)
|
|
||||||
await expect(
|
|
||||||
contracts.newContract(
|
|
||||||
request.duration,
|
|
||||||
request.size,
|
|
||||||
request.contentHash,
|
|
||||||
request.proofPeriod,
|
|
||||||
request.proofTimeout,
|
|
||||||
request.nonce,
|
|
||||||
bid.price,
|
|
||||||
await host.getAddress(),
|
|
||||||
bid.bidExpiry,
|
|
||||||
await sign(client, requestHash),
|
|
||||||
await sign(host, bidHash)
|
|
||||||
)
|
|
||||||
).to.be.revertedWith("Contract already exists")
|
|
||||||
})
|
|
||||||
|
|
||||||
it("cannot be created when client signature is invalid", async function () {
|
|
||||||
let invalidHash = hashRequest({
|
|
||||||
...request,
|
|
||||||
duration: request.duration + 1,
|
|
||||||
})
|
|
||||||
let invalidSignature = await sign(client, invalidHash)
|
|
||||||
await expect(
|
|
||||||
contracts.newContract(
|
|
||||||
request.duration,
|
|
||||||
request.size,
|
|
||||||
request.contentHash,
|
|
||||||
request.proofPeriod,
|
|
||||||
request.proofTimeout,
|
|
||||||
request.nonce,
|
|
||||||
bid.price,
|
|
||||||
await host.getAddress(),
|
|
||||||
bid.bidExpiry,
|
|
||||||
invalidSignature,
|
|
||||||
await sign(host, bidHash)
|
|
||||||
)
|
|
||||||
).to.be.revertedWith("Invalid signature")
|
|
||||||
})
|
|
||||||
|
|
||||||
it("cannot be created when host signature is invalid", async function () {
|
|
||||||
let invalidBid = hashBid({ ...bid, requestHash, price: bid.price - 1 })
|
|
||||||
let invalidSignature = await sign(host, invalidBid)
|
|
||||||
await expect(
|
|
||||||
contracts.newContract(
|
|
||||||
request.duration,
|
|
||||||
request.size,
|
|
||||||
request.contentHash,
|
|
||||||
request.proofPeriod,
|
|
||||||
request.proofTimeout,
|
|
||||||
request.nonce,
|
|
||||||
bid.price,
|
|
||||||
await host.getAddress(),
|
|
||||||
bid.bidExpiry,
|
|
||||||
await sign(client, requestHash),
|
|
||||||
invalidSignature
|
|
||||||
)
|
|
||||||
).to.be.revertedWith("Invalid signature")
|
|
||||||
})
|
|
||||||
|
|
||||||
it("cannot be created when bid has expired", async function () {
|
|
||||||
let expired = Math.round(Date.now() / 1000) - 60 // 1 minute ago
|
|
||||||
let bidHash = hashBid({ ...bid, requestHash, bidExpiry: expired })
|
|
||||||
await expect(
|
|
||||||
contracts.newContract(
|
|
||||||
request.duration,
|
|
||||||
request.size,
|
|
||||||
request.contentHash,
|
|
||||||
request.proofPeriod,
|
|
||||||
request.proofTimeout,
|
|
||||||
request.nonce,
|
|
||||||
bid.price,
|
|
||||||
await host.getAddress(),
|
|
||||||
expired,
|
|
||||||
await sign(client, requestHash),
|
|
||||||
await sign(host, bidHash)
|
|
||||||
)
|
|
||||||
).to.be.revertedWith("Bid expired")
|
|
||||||
})
|
|
||||||
})
|
|
@ -2,7 +2,7 @@ const { ethers } = require("hardhat")
|
|||||||
const { expect } = require("chai")
|
const { expect } = require("chai")
|
||||||
const { exampleRequest, exampleOffer } = require("./examples")
|
const { exampleRequest, exampleOffer } = require("./examples")
|
||||||
const { now, hours } = require("./time")
|
const { now, hours } = require("./time")
|
||||||
const { keccak256, defaultAbiCoder } = ethers.utils
|
const { requestId, offerId, requestToArray, offerToArray } = require("./ids")
|
||||||
|
|
||||||
describe("Marketplace", function () {
|
describe("Marketplace", function () {
|
||||||
const collateral = 100
|
const collateral = 100
|
||||||
@ -224,49 +224,3 @@ describe("Marketplace", function () {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
function requestId(request) {
|
|
||||||
return keccak256(
|
|
||||||
defaultAbiCoder.encode(
|
|
||||||
[
|
|
||||||
"address",
|
|
||||||
"uint256",
|
|
||||||
"uint256",
|
|
||||||
"bytes32",
|
|
||||||
"uint256",
|
|
||||||
"uint256",
|
|
||||||
"uint256",
|
|
||||||
"uint256",
|
|
||||||
"bytes32",
|
|
||||||
],
|
|
||||||
requestToArray(request)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function offerId(offer) {
|
|
||||||
return keccak256(
|
|
||||||
defaultAbiCoder.encode(
|
|
||||||
["address", "bytes32", "uint256", "uint256"],
|
|
||||||
offerToArray(offer)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function requestToArray(request) {
|
|
||||||
return [
|
|
||||||
request.client,
|
|
||||||
request.duration,
|
|
||||||
request.size,
|
|
||||||
request.contentHash,
|
|
||||||
request.proofPeriod,
|
|
||||||
request.proofTimeout,
|
|
||||||
request.maxPrice,
|
|
||||||
request.expiry,
|
|
||||||
request.nonce,
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
function offerToArray(offer) {
|
|
||||||
return [offer.host, offer.requestId, offer.price, offer.expiry]
|
|
||||||
}
|
|
||||||
|
@ -1,198 +1,136 @@
|
|||||||
const { expect } = require("chai")
|
const { expect } = require("chai")
|
||||||
const { ethers, deployments } = require("hardhat")
|
const { ethers, deployments } = require("hardhat")
|
||||||
const { hashRequest, hashBid, sign } = require("./marketplace")
|
const { exampleRequest, exampleOffer } = require("./examples")
|
||||||
const { exampleRequest, exampleBid } = require("./examples")
|
|
||||||
const { mineBlock, minedBlockNumber } = require("./mining")
|
const { mineBlock, minedBlockNumber } = require("./mining")
|
||||||
|
const { requestId, offerId } = require("./ids")
|
||||||
|
|
||||||
describe("Storage", function () {
|
describe("Storage", function () {
|
||||||
const request = exampleRequest()
|
|
||||||
const bid = exampleBid()
|
|
||||||
|
|
||||||
let storage
|
let storage
|
||||||
let token
|
let token
|
||||||
let client, host
|
let client, host
|
||||||
|
let request, offer
|
||||||
let collateralAmount, slashMisses, slashPercentage
|
let collateralAmount, slashMisses, slashPercentage
|
||||||
|
let id
|
||||||
|
|
||||||
|
function switchAccount(account) {
|
||||||
|
token = token.connect(account)
|
||||||
|
storage = storage.connect(account)
|
||||||
|
}
|
||||||
|
|
||||||
beforeEach(async function () {
|
beforeEach(async function () {
|
||||||
;[client, host] = await ethers.getSigners()
|
;[client, host] = await ethers.getSigners()
|
||||||
|
|
||||||
await deployments.fixture(["TestToken", "Storage"])
|
await deployments.fixture(["TestToken", "Storage"])
|
||||||
token = await ethers.getContract("TestToken")
|
token = await ethers.getContract("TestToken")
|
||||||
storage = await ethers.getContract("Storage")
|
storage = await ethers.getContract("Storage")
|
||||||
|
|
||||||
await token.mint(client.address, 1000)
|
await token.mint(client.address, 1000)
|
||||||
await token.mint(host.address, 1000)
|
await token.mint(host.address, 1000)
|
||||||
|
|
||||||
collateralAmount = await storage.collateralAmount()
|
collateralAmount = await storage.collateralAmount()
|
||||||
slashMisses = await storage.slashMisses()
|
slashMisses = await storage.slashMisses()
|
||||||
slashPercentage = await storage.slashPercentage()
|
slashPercentage = await storage.slashPercentage()
|
||||||
|
|
||||||
|
request = exampleRequest()
|
||||||
|
request.client = client.address
|
||||||
|
|
||||||
|
offer = exampleOffer()
|
||||||
|
offer.host = host.address
|
||||||
|
offer.requestId = requestId(request)
|
||||||
|
|
||||||
|
switchAccount(client)
|
||||||
|
await token.approve(storage.address, request.maxPrice)
|
||||||
|
await storage.requestStorage(request)
|
||||||
|
switchAccount(host)
|
||||||
|
await token.approve(storage.address, collateralAmount)
|
||||||
|
await storage.deposit(collateralAmount)
|
||||||
|
await storage.offerStorage(offer)
|
||||||
|
switchAccount(client)
|
||||||
|
await storage.selectOffer(offerId(offer))
|
||||||
|
id = offerId(offer)
|
||||||
})
|
})
|
||||||
|
|
||||||
describe("creating a new storage contract", function () {
|
describe("starting the contract", function () {
|
||||||
let id
|
it("starts requiring storage proofs", async function () {
|
||||||
|
switchAccount(host)
|
||||||
|
await storage.startContract(id)
|
||||||
|
expect(await storage.proofEnd(id)).to.be.gt(0)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("can only be done by the host", async function () {
|
||||||
|
switchAccount(client)
|
||||||
|
await expect(storage.startContract(id)).to.be.revertedWith(
|
||||||
|
"Only host can call this function"
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("can only be done once", async function () {
|
||||||
|
switchAccount(host)
|
||||||
|
await storage.startContract(id)
|
||||||
|
await expect(storage.startContract(id)).to.be.reverted
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("finishing the contract", function () {
|
||||||
beforeEach(async function () {
|
beforeEach(async function () {
|
||||||
await token.connect(host).approve(storage.address, collateralAmount)
|
switchAccount(host)
|
||||||
await token.connect(client).approve(storage.address, bid.price)
|
await storage.startContract(id)
|
||||||
await storage.connect(host).deposit(collateralAmount)
|
|
||||||
let requestHash = hashRequest(request)
|
|
||||||
let bidHash = hashBid({ ...bid, requestHash })
|
|
||||||
await storage.newContract(
|
|
||||||
request.duration,
|
|
||||||
request.size,
|
|
||||||
request.contentHash,
|
|
||||||
request.proofPeriod,
|
|
||||||
request.proofTimeout,
|
|
||||||
request.nonce,
|
|
||||||
bid.price,
|
|
||||||
await host.getAddress(),
|
|
||||||
bid.bidExpiry,
|
|
||||||
await sign(client, requestHash),
|
|
||||||
await sign(host, bidHash)
|
|
||||||
)
|
|
||||||
id = bidHash
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it("created the contract", async function () {
|
async function mineUntilEnd() {
|
||||||
expect(await storage.duration(id)).to.equal(request.duration)
|
const end = await storage.proofEnd(id)
|
||||||
expect(await storage.size(id)).to.equal(request.size)
|
while ((await minedBlockNumber()) < end) {
|
||||||
expect(await storage.contentHash(id)).to.equal(request.contentHash)
|
await mineBlock()
|
||||||
expect(await storage.proofPeriod(id)).to.equal(request.proofPeriod)
|
}
|
||||||
expect(await storage.proofTimeout(id)).to.equal(request.proofTimeout)
|
}
|
||||||
expect(await storage.price(id)).to.equal(bid.price)
|
|
||||||
expect(await storage.host(id)).to.equal(await host.getAddress())
|
it("pays the host", async function () {
|
||||||
|
await mineUntilEnd()
|
||||||
|
const startBalance = await token.balanceOf(host.address)
|
||||||
|
await storage.finishContract(id)
|
||||||
|
const endBalance = await token.balanceOf(host.address)
|
||||||
|
expect(endBalance - startBalance).to.equal(offer.price)
|
||||||
})
|
})
|
||||||
|
|
||||||
it("locks up host collateral", async function () {
|
it("is only allowed when end time has passed", async function () {
|
||||||
await expect(storage.connect(host).withdraw()).to.be.revertedWith(
|
await expect(storage.finishContract(id)).to.be.revertedWith(
|
||||||
"Account locked"
|
"Contract has not ended yet"
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
describe("starting the contract", function () {
|
it("can only be done once", async function () {
|
||||||
it("starts requiring storage proofs", async function () {
|
await mineUntilEnd()
|
||||||
await storage.connect(host).startContract(id)
|
await storage.finishContract(id)
|
||||||
expect(await storage.proofEnd(id)).to.be.gt(0)
|
await expect(storage.finishContract(id)).to.be.revertedWith(
|
||||||
})
|
"Contract already finished"
|
||||||
|
)
|
||||||
it("can only be done by the host", async function () {
|
|
||||||
await expect(
|
|
||||||
storage.connect(client).startContract(id)
|
|
||||||
).to.be.revertedWith("Only host can call this function")
|
|
||||||
})
|
|
||||||
|
|
||||||
it("can only be done once", async function () {
|
|
||||||
await storage.connect(host).startContract(id)
|
|
||||||
await expect(storage.connect(host).startContract(id)).to.be.reverted
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe("finishing the contract", function () {
|
|
||||||
beforeEach(async function () {
|
|
||||||
await storage.connect(host).startContract(id)
|
|
||||||
})
|
|
||||||
|
|
||||||
async function mineUntilEnd() {
|
|
||||||
const end = await storage.proofEnd(id)
|
|
||||||
while ((await minedBlockNumber()) < end) {
|
|
||||||
await mineBlock()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
it("unlocks the host collateral", async function () {
|
|
||||||
await mineUntilEnd()
|
|
||||||
await storage.finishContract(id)
|
|
||||||
await expect(storage.connect(host).withdraw()).not.to.be.reverted
|
|
||||||
})
|
|
||||||
|
|
||||||
it("pays the host", async function () {
|
|
||||||
await mineUntilEnd()
|
|
||||||
const startBalance = await token.balanceOf(host.address)
|
|
||||||
await storage.finishContract(id)
|
|
||||||
const endBalance = await token.balanceOf(host.address)
|
|
||||||
expect(endBalance - startBalance).to.equal(bid.price)
|
|
||||||
})
|
|
||||||
|
|
||||||
it("is only allowed when end time has passed", async function () {
|
|
||||||
await expect(storage.finishContract(id)).to.be.revertedWith(
|
|
||||||
"Contract has not ended yet"
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
it("can only be done once", async function () {
|
|
||||||
await mineUntilEnd()
|
|
||||||
await storage.finishContract(id)
|
|
||||||
await expect(storage.finishContract(id)).to.be.revertedWith(
|
|
||||||
"Contract already finished"
|
|
||||||
)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe("slashing when missing proofs", function () {
|
|
||||||
async function ensureProofIsMissing() {
|
|
||||||
while (!(await storage.isProofRequired(id, await minedBlockNumber()))) {
|
|
||||||
mineBlock()
|
|
||||||
}
|
|
||||||
const blocknumber = await minedBlockNumber()
|
|
||||||
for (let i = 0; i < request.proofTimeout; i++) {
|
|
||||||
mineBlock()
|
|
||||||
}
|
|
||||||
await storage.markProofAsMissing(id, blocknumber)
|
|
||||||
}
|
|
||||||
|
|
||||||
it("reduces collateral when too many proofs are missing", async function () {
|
|
||||||
await storage.connect(host).startContract(id)
|
|
||||||
for (let i = 0; i < slashMisses; i++) {
|
|
||||||
await ensureProofIsMissing()
|
|
||||||
}
|
|
||||||
const expectedBalance =
|
|
||||||
(collateralAmount * (100 - slashPercentage)) / 100
|
|
||||||
expect(await storage.balanceOf(host.address)).to.equal(expectedBalance)
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
it("doesn't create contract with insufficient collateral", async function () {
|
describe("slashing when missing proofs", function () {
|
||||||
await token.connect(host).approve(storage.address, collateralAmount - 1)
|
beforeEach(function () {
|
||||||
await token.connect(client).approve(storage.address, bid.price)
|
switchAccount(host)
|
||||||
await storage.connect(host).deposit(collateralAmount - 1)
|
})
|
||||||
let requestHash = hashRequest(request)
|
|
||||||
let bidHash = hashBid({ ...bid, requestHash })
|
|
||||||
await expect(
|
|
||||||
storage.newContract(
|
|
||||||
request.duration,
|
|
||||||
request.size,
|
|
||||||
request.contentHash,
|
|
||||||
request.proofPeriod,
|
|
||||||
request.proofTimeout,
|
|
||||||
request.nonce,
|
|
||||||
bid.price,
|
|
||||||
await host.getAddress(),
|
|
||||||
bid.bidExpiry,
|
|
||||||
await sign(client, requestHash),
|
|
||||||
await sign(host, bidHash)
|
|
||||||
)
|
|
||||||
).to.be.revertedWith("Insufficient collateral")
|
|
||||||
})
|
|
||||||
|
|
||||||
it("doesn't create contract without payment of price", async function () {
|
async function ensureProofIsMissing() {
|
||||||
await token.connect(host).approve(storage.address, collateralAmount)
|
while (!(await storage.isProofRequired(id, await minedBlockNumber()))) {
|
||||||
await token.connect(client).approve(storage.address, bid.price - 1)
|
mineBlock()
|
||||||
await storage.connect(host).deposit(collateralAmount)
|
}
|
||||||
let requestHash = hashRequest(request)
|
const blocknumber = await minedBlockNumber()
|
||||||
let bidHash = hashBid({ ...bid, requestHash })
|
for (let i = 0; i < request.proofTimeout; i++) {
|
||||||
await expect(
|
mineBlock()
|
||||||
storage.newContract(
|
}
|
||||||
request.duration,
|
await storage.markProofAsMissing(id, blocknumber)
|
||||||
request.size,
|
}
|
||||||
request.contentHash,
|
|
||||||
request.proofPeriod,
|
it("reduces collateral when too many proofs are missing", async function () {
|
||||||
request.proofTimeout,
|
await storage.connect(host).startContract(id)
|
||||||
request.nonce,
|
for (let i = 0; i < slashMisses; i++) {
|
||||||
bid.price,
|
await ensureProofIsMissing()
|
||||||
await host.getAddress(),
|
}
|
||||||
bid.bidExpiry,
|
const expectedBalance = (collateralAmount * (100 - slashPercentage)) / 100
|
||||||
await sign(client, requestHash),
|
expect(await storage.balanceOf(host.address)).to.equal(expectedBalance)
|
||||||
await sign(host, bidHash)
|
})
|
||||||
)
|
|
||||||
).to.be.revertedWith("ERC20: transfer amount exceeds allowance")
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
50
test/ids.js
Normal file
50
test/ids.js
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
const { ethers } = require("hardhat")
|
||||||
|
const { keccak256, defaultAbiCoder } = ethers.utils
|
||||||
|
|
||||||
|
function requestId(request) {
|
||||||
|
return keccak256(
|
||||||
|
defaultAbiCoder.encode(
|
||||||
|
[
|
||||||
|
"address",
|
||||||
|
"uint256",
|
||||||
|
"uint256",
|
||||||
|
"bytes32",
|
||||||
|
"uint256",
|
||||||
|
"uint256",
|
||||||
|
"uint256",
|
||||||
|
"uint256",
|
||||||
|
"bytes32",
|
||||||
|
],
|
||||||
|
requestToArray(request)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function offerId(offer) {
|
||||||
|
return keccak256(
|
||||||
|
defaultAbiCoder.encode(
|
||||||
|
["address", "bytes32", "uint256", "uint256"],
|
||||||
|
offerToArray(offer)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function requestToArray(request) {
|
||||||
|
return [
|
||||||
|
request.client,
|
||||||
|
request.duration,
|
||||||
|
request.size,
|
||||||
|
request.contentHash,
|
||||||
|
request.proofPeriod,
|
||||||
|
request.proofTimeout,
|
||||||
|
request.maxPrice,
|
||||||
|
request.expiry,
|
||||||
|
request.nonce,
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
function offerToArray(offer) {
|
||||||
|
return [offer.host, offer.requestId, offer.price, offer.expiry]
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { requestId, offerId, requestToArray, offerToArray }
|
@ -1,34 +0,0 @@
|
|||||||
const { ethers } = require("hardhat")
|
|
||||||
|
|
||||||
function hashRequest({
|
|
||||||
duration,
|
|
||||||
size,
|
|
||||||
contentHash,
|
|
||||||
proofPeriod,
|
|
||||||
proofTimeout,
|
|
||||||
nonce,
|
|
||||||
}) {
|
|
||||||
const type = "[dagger.request.v1]"
|
|
||||||
return ethers.utils.keccak256(
|
|
||||||
ethers.utils.defaultAbiCoder.encode(
|
|
||||||
["string", "uint", "uint", "bytes32", "uint", "uint", "bytes32"],
|
|
||||||
[type, duration, size, contentHash, proofPeriod, proofTimeout, nonce]
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function hashBid({ requestHash, bidExpiry, price }) {
|
|
||||||
const type = "[dagger.bid.v1]"
|
|
||||||
return ethers.utils.keccak256(
|
|
||||||
ethers.utils.defaultAbiCoder.encode(
|
|
||||||
["string", "bytes32", "uint", "uint"],
|
|
||||||
[type, requestHash, bidExpiry, price]
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
async function sign(signer, hash) {
|
|
||||||
return await signer.signMessage(ethers.utils.arrayify(hash))
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = { hashRequest, hashBid, sign }
|
|
Loading…
x
Reference in New Issue
Block a user