Remove Contracts in favor of Marketplace

This commit is contained in:
Mark Spanbroek 2022-02-22 09:25:42 +01:00 committed by markspanbroek
parent 7e7134b99d
commit e818d70b85
9 changed files with 173 additions and 647 deletions

View File

@ -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");
}
}

View File

@ -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;

View File

@ -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);
}
}
}

View File

@ -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);
}
}

View File

@ -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")
})
})

View File

@ -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]
}

View File

@ -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)
})
})
})

50
test/ids.js Normal file
View 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 }

View File

@ -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 }