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);
|
||||
}
|
||||
|
||||
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 {
|
||||
address client;
|
||||
uint256 duration;
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
// SPDX-License-Identifier: MIT
|
||||
pragma solidity ^0.8.0;
|
||||
|
||||
import "./Contracts.sol";
|
||||
import "./Marketplace.sol";
|
||||
import "./Proofs.sol";
|
||||
import "./Collateral.sol";
|
||||
|
||||
contract Storage is Contracts, Proofs, Collateral {
|
||||
contract Storage is Collateral, Marketplace, Proofs {
|
||||
uint256 public collateralAmount;
|
||||
uint256 public slashMisses;
|
||||
uint256 public slashPercentage;
|
||||
|
@ -17,96 +17,30 @@ contract Storage is Contracts, Proofs, Collateral {
|
|||
uint256 _collateralAmount,
|
||||
uint256 _slashMisses,
|
||||
uint256 _slashPercentage
|
||||
) Collateral(token) {
|
||||
) Marketplace(token, _collateralAmount) {
|
||||
collateralAmount = _collateralAmount;
|
||||
slashMisses = _slashMisses;
|
||||
slashPercentage = _slashPercentage;
|
||||
}
|
||||
|
||||
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 {
|
||||
require(balanceOf(_host) >= collateralAmount, "Insufficient collateral");
|
||||
bytes32 requestHash = _hashRequest(
|
||||
_duration,
|
||||
_size,
|
||||
_contentHash,
|
||||
_proofPeriod,
|
||||
_proofTimeout,
|
||||
_nonce
|
||||
function startContract(bytes32 id) public {
|
||||
Offer storage offer = _offer(id);
|
||||
require(msg.sender == offer.host, "Only host can call this function");
|
||||
Request storage request = _request(offer.requestId);
|
||||
_expectProofs(
|
||||
id,
|
||||
request.proofPeriod,
|
||||
request.proofTimeout,
|
||||
request.duration
|
||||
);
|
||||
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 {
|
||||
require(block.number > proofEnd(id), "Contract has not ended yet");
|
||||
require(!finished[id], "Contract already finished");
|
||||
_unlock(id);
|
||||
finished[id] = true;
|
||||
require(token.transfer(host(id), price(id)), "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);
|
||||
Offer storage offer = _offer(id);
|
||||
require(token.transfer(offer.host, offer.price), "Payment failed");
|
||||
}
|
||||
|
||||
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 {
|
||||
_markProofAsMissing(contractId, blocknumber);
|
||||
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 { exampleRequest, exampleOffer } = require("./examples")
|
||||
const { now, hours } = require("./time")
|
||||
const { keccak256, defaultAbiCoder } = ethers.utils
|
||||
const { requestId, offerId, requestToArray, offerToArray } = require("./ids")
|
||||
|
||||
describe("Marketplace", function () {
|
||||
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 { ethers, deployments } = require("hardhat")
|
||||
const { hashRequest, hashBid, sign } = require("./marketplace")
|
||||
const { exampleRequest, exampleBid } = require("./examples")
|
||||
const { exampleRequest, exampleOffer } = require("./examples")
|
||||
const { mineBlock, minedBlockNumber } = require("./mining")
|
||||
const { requestId, offerId } = require("./ids")
|
||||
|
||||
describe("Storage", function () {
|
||||
const request = exampleRequest()
|
||||
const bid = exampleBid()
|
||||
|
||||
let storage
|
||||
let token
|
||||
let client, host
|
||||
let request, offer
|
||||
let collateralAmount, slashMisses, slashPercentage
|
||||
let id
|
||||
|
||||
function switchAccount(account) {
|
||||
token = token.connect(account)
|
||||
storage = storage.connect(account)
|
||||
}
|
||||
|
||||
beforeEach(async function () {
|
||||
;[client, host] = await ethers.getSigners()
|
||||
|
||||
await deployments.fixture(["TestToken", "Storage"])
|
||||
token = await ethers.getContract("TestToken")
|
||||
storage = await ethers.getContract("Storage")
|
||||
|
||||
await token.mint(client.address, 1000)
|
||||
await token.mint(host.address, 1000)
|
||||
|
||||
collateralAmount = await storage.collateralAmount()
|
||||
slashMisses = await storage.slashMisses()
|
||||
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 () {
|
||||
let id
|
||||
describe("starting the contract", function () {
|
||||
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 () {
|
||||
await token.connect(host).approve(storage.address, collateralAmount)
|
||||
await token.connect(client).approve(storage.address, bid.price)
|
||||
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
|
||||
switchAccount(host)
|
||||
await storage.startContract(id)
|
||||
})
|
||||
|
||||
it("created the contract", async function () {
|
||||
expect(await storage.duration(id)).to.equal(request.duration)
|
||||
expect(await storage.size(id)).to.equal(request.size)
|
||||
expect(await storage.contentHash(id)).to.equal(request.contentHash)
|
||||
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())
|
||||
async function mineUntilEnd() {
|
||||
const end = await storage.proofEnd(id)
|
||||
while ((await minedBlockNumber()) < end) {
|
||||
await mineBlock()
|
||||
}
|
||||
}
|
||||
|
||||
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 () {
|
||||
await expect(storage.connect(host).withdraw()).to.be.revertedWith(
|
||||
"Account locked"
|
||||
it("is only allowed when end time has passed", async function () {
|
||||
await expect(storage.finishContract(id)).to.be.revertedWith(
|
||||
"Contract has not ended yet"
|
||||
)
|
||||
})
|
||||
|
||||
describe("starting the contract", function () {
|
||||
it("starts requiring storage proofs", async function () {
|
||||
await storage.connect(host).startContract(id)
|
||||
expect(await storage.proofEnd(id)).to.be.gt(0)
|
||||
})
|
||||
|
||||
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("can only be done once", async function () {
|
||||
await mineUntilEnd()
|
||||
await storage.finishContract(id)
|
||||
await expect(storage.finishContract(id)).to.be.revertedWith(
|
||||
"Contract already finished"
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
it("doesn't create contract with insufficient collateral", async function () {
|
||||
await token.connect(host).approve(storage.address, collateralAmount - 1)
|
||||
await token.connect(client).approve(storage.address, bid.price)
|
||||
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")
|
||||
})
|
||||
describe("slashing when missing proofs", function () {
|
||||
beforeEach(function () {
|
||||
switchAccount(host)
|
||||
})
|
||||
|
||||
it("doesn't create contract without payment of price", async function () {
|
||||
await token.connect(host).approve(storage.address, collateralAmount)
|
||||
await token.connect(client).approve(storage.address, bid.price - 1)
|
||||
await storage.connect(host).deposit(collateralAmount)
|
||||
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("ERC20: transfer amount exceeds allowance")
|
||||
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)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
|
|
|
@ -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…
Reference in New Issue