From e963a25c943fee5d243e42d8e06721ef60128b35 Mon Sep 17 00:00:00 2001 From: Mark Spanbroek Date: Tue, 15 Feb 2022 17:54:19 +0100 Subject: [PATCH] Replace Stakes with Collateral Removes the old Stakes implementation in favor of the new Collateral implementation. --- Readme.md | 14 ++++---- contracts/Collateral.sol | 2 +- contracts/Contracts.sol | 4 +-- contracts/Stakes.sol | 45 ----------------------- contracts/Storage.sol | 44 +++++++++++------------ contracts/TestStakes.sol | 34 ------------------ deploy/storage.js | 4 +-- test/Stakes.test.js | 78 ---------------------------------------- test/Storage.test.js | 37 +++++++++---------- 9 files changed, 52 insertions(+), 210 deletions(-) delete mode 100644 contracts/Stakes.sol delete mode 100644 contracts/TestStakes.sol delete mode 100644 test/Stakes.test.js diff --git a/Readme.md b/Readme.md index e928ae6..f9ace67 100644 --- a/Readme.md +++ b/Readme.md @@ -3,7 +3,7 @@ Dagger Contracts An experimental implementation of the contracts that underly the Dagger storage network. Its goal is to experiment with the rules around the bidding process, -the storage contracts, the storage proofs and the host stakes. Neither +the storage contracts, the storage proofs and the host collateral. Neither completeness nor correctness are guaranteed at this moment in time. Running @@ -76,14 +76,14 @@ When a new storage contract is created the client immediately pays the entire price of the contract. The payment is only released to the host upon successful completion of the contract. -Stakes +Collateral ------ -To motivate a host to remain honest, it must put up some collateral (stake) -before it is allowed to participate in storage contracts. The stake may not be -withdrawn as long as a host is participating in an active storage contract. +To motivate a host to remain honest, it must put up some collateral before it is +allowed to participate in storage contracts. The collateral may not be withdrawn +as long as a host is participating in an active storage contract. -Should a host be misbehaving, then its stake may be reduced by a certain +Should a host be misbehaving, then its collateral may be reduced by a certain percentage (slashed). Proofs @@ -104,7 +104,7 @@ by the client and host during the request/bid exchange. Hosts have a small period of time in which they are expected to submit a proof. When that time has expired without seeing a proof, validators are able to point out the lack of proof. If a host misses too many proofs, it results into a -slashing of its stake. +slashing of its collateral. To Do ----- diff --git a/contracts/Collateral.sol b/contracts/Collateral.sol index f65df55..7ad8a3a 100644 --- a/contracts/Collateral.sol +++ b/contracts/Collateral.sol @@ -5,7 +5,7 @@ import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import "./AccountLocks.sol"; contract Collateral is AccountLocks { - IERC20 private immutable token; + IERC20 public immutable token; Totals private totals; mapping(address => uint256) private balances; diff --git a/contracts/Contracts.sol b/contracts/Contracts.sol index 9a46af5..515c2fd 100644 --- a/contracts/Contracts.sol +++ b/contracts/Contracts.sol @@ -86,7 +86,7 @@ contract Contracts { uint256 proofPeriod, uint256 proofTimeout, bytes32 nonce - ) private pure returns (bytes32) { + ) internal pure returns (bytes32) { return keccak256( abi.encode( @@ -106,7 +106,7 @@ contract Contracts { bytes32 requestHash, uint256 expiry, uint256 price - ) private pure returns (bytes32) { + ) internal pure returns (bytes32) { return keccak256(abi.encode("[dagger.bid.v1]", requestHash, expiry, price)); } diff --git a/contracts/Stakes.sol b/contracts/Stakes.sol deleted file mode 100644 index 8cf9fad..0000000 --- a/contracts/Stakes.sol +++ /dev/null @@ -1,45 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; - -import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; - -contract Stakes { - IERC20 private token; - mapping(address => uint256) private stakes; - mapping(address => uint256) private locks; - - constructor(IERC20 __token) { - token = __token; - } - - function _token() internal view returns (IERC20) { - return token; - } - - function _stake(address account) internal view returns (uint256) { - return stakes[account]; - } - - function _increaseStake(uint256 amount) internal { - token.transferFrom(msg.sender, address(this), amount); - stakes[msg.sender] += amount; - } - - function _withdrawStake() internal { - require(locks[msg.sender] == 0, "Stake locked"); - token.transfer(msg.sender, stakes[msg.sender]); - } - - function _lockStake(address account) internal { - locks[account] += 1; - } - - function _unlockStake(address account) internal { - require(locks[account] > 0, "Stake already unlocked"); - locks[account] -= 1; - } - - function _slash(address account, uint256 percentage) internal { - stakes[account] = (stakes[account] * (100 - percentage)) / 100; - } -} diff --git a/contracts/Storage.sol b/contracts/Storage.sol index 60c6f12..bc73605 100644 --- a/contracts/Storage.sol +++ b/contracts/Storage.sol @@ -3,10 +3,10 @@ pragma solidity ^0.8.0; import "./Contracts.sol"; import "./Proofs.sol"; -import "./Stakes.sol"; +import "./Collateral.sol"; -contract Storage is Contracts, Proofs, Stakes { - uint256 public stakeAmount; +contract Storage is Contracts, Proofs, Collateral { + uint256 public collateralAmount; uint256 public slashMisses; uint256 public slashPercentage; @@ -14,11 +14,11 @@ contract Storage is Contracts, Proofs, Stakes { constructor( IERC20 token, - uint256 _stakeAmount, + uint256 _collateralAmount, uint256 _slashMisses, uint256 _slashPercentage - ) Stakes(token) { - stakeAmount = _stakeAmount; + ) Collateral(token) { + collateralAmount = _collateralAmount; slashMisses = _slashMisses; slashPercentage = _slashPercentage; } @@ -36,9 +36,19 @@ contract Storage is Contracts, Proofs, Stakes { bytes memory requestSignature, bytes memory bidSignature ) public { - require(_stake(_host) >= stakeAmount, "Insufficient stake"); - _lockStake(_host); - _token().transferFrom(msg.sender, address(this), _price); + 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, @@ -66,9 +76,9 @@ contract Storage is Contracts, Proofs, Stakes { function finishContract(bytes32 id) public { require(block.number > proofEnd(id), "Contract has not ended yet"); require(!finished[id], "Contract already finished"); - _unlockStake(host(id)); + _unlock(id); finished[id] = true; - require(_token().transfer(host(id), price(id)), "Payment failed"); + require(token.transfer(host(id), price(id)), "Payment failed"); } function duration(bytes32 contractId) public view returns (uint256) { @@ -107,10 +117,6 @@ contract Storage is Contracts, Proofs, Stakes { return _missed(contractId); } - function stake(address account) public view returns (uint256) { - return _stake(account); - } - function isProofRequired(bytes32 contractId, uint256 blocknumber) public view @@ -141,12 +147,4 @@ contract Storage is Contracts, Proofs, Stakes { _slash(host(contractId), slashPercentage); } } - - function increaseStake(uint256 amount) public { - _increaseStake(amount); - } - - function withdrawStake() public { - _withdrawStake(); - } } diff --git a/contracts/TestStakes.sol b/contracts/TestStakes.sol deleted file mode 100644 index 47d66e8..0000000 --- a/contracts/TestStakes.sol +++ /dev/null @@ -1,34 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; - -import "./Stakes.sol"; - -// exposes internal functions of Stakes for testing -contract TestStakes is Stakes { - // solhint-disable-next-line no-empty-blocks - constructor(IERC20 token) Stakes(token) {} - - function stake(address account) public view returns (uint256) { - return _stake(account); - } - - function increaseStake(uint256 amount) public { - _increaseStake(amount); - } - - function withdrawStake() public { - _withdrawStake(); - } - - function lockStake(address account) public { - _lockStake(account); - } - - function unlockStake(address account) public { - _unlockStake(account); - } - - function slash(address account, uint256 percentage) public { - _slash(account, percentage); - } -} diff --git a/deploy/storage.js b/deploy/storage.js index e7d0785..d333b4c 100644 --- a/deploy/storage.js +++ b/deploy/storage.js @@ -1,9 +1,9 @@ module.exports = async ({ deployments, getNamedAccounts }) => { const token = await deployments.get("TestToken") - const stakeAmount = 100 + const collateralAmount = 100 const slashMisses = 3 const slashPercentage = 10 - const args = [token.address, stakeAmount, slashMisses, slashPercentage] + const args = [token.address, collateralAmount, slashMisses, slashPercentage] const { deployer } = await getNamedAccounts() await deployments.deploy("Storage", { args, from: deployer }) } diff --git a/test/Stakes.test.js b/test/Stakes.test.js deleted file mode 100644 index f8ac8d5..0000000 --- a/test/Stakes.test.js +++ /dev/null @@ -1,78 +0,0 @@ -const { expect } = require("chai") -const { ethers } = require("hardhat") - -describe("Stakes", function () { - var stakes - var token - var host - - beforeEach(async function () { - ;[host] = await ethers.getSigners() - const Stakes = await ethers.getContractFactory("TestStakes") - const TestToken = await ethers.getContractFactory("TestToken") - token = await TestToken.deploy() - stakes = await Stakes.deploy(token.address) - await token.mint(host.address, 1000) - }) - - it("has zero stakes initially", async function () { - const address = await host.getAddress() - const stake = await stakes.stake(address) - expect(stake).to.equal(0) - }) - - it("increases stakes by transferring tokens", async function () { - await token.approve(stakes.address, 20) - await stakes.increaseStake(20) - let stake = await stakes.stake(host.address) - expect(stake).to.equal(20) - }) - - it("does not increase stake when token transfer fails", async function () { - await expect(stakes.increaseStake(20)).to.be.revertedWith( - "ERC20: transfer amount exceeds allowance" - ) - }) - - it("allows withdrawal of stake", async function () { - await token.approve(stakes.address, 20) - await stakes.increaseStake(20) - let balanceBefore = await token.balanceOf(host.address) - await stakes.withdrawStake() - let balanceAfter = await token.balanceOf(host.address) - expect(balanceAfter - balanceBefore).to.equal(20) - }) - - it("locks stake", async function () { - await token.approve(stakes.address, 20) - await stakes.increaseStake(20) - await stakes.lockStake(host.address) - await expect(stakes.withdrawStake()).to.be.revertedWith("Stake locked") - await stakes.unlockStake(host.address) - await expect(stakes.withdrawStake()).not.to.be.reverted - }) - - it("fails to unlock when already unlocked", async function () { - await expect(stakes.unlockStake(host.address)).to.be.revertedWith( - "Stake already unlocked" - ) - }) - - it("requires an equal amount of locks and unlocks", async function () { - await token.approve(stakes.address, 20) - await stakes.increaseStake(20) - await stakes.lockStake(host.address) - await stakes.lockStake(host.address) - await stakes.unlockStake(host.address) - await expect(stakes.withdrawStake()).to.be.revertedWith("Stake locked") - await stakes.unlockStake(host.address) - await expect(stakes.withdrawStake()).not.to.be.reverted - }) - - it("slashes stake", async function () { - await token.approve(stakes.address, 1000) - await stakes.increaseStake(1000) - await stakes.slash(host.address, 10) - expect(await stakes.stake(host.address)).to.equal(900) - }) -}) diff --git a/test/Storage.test.js b/test/Storage.test.js index 9221b95..bd04789 100644 --- a/test/Storage.test.js +++ b/test/Storage.test.js @@ -11,7 +11,7 @@ describe("Storage", function () { let storage let token let client, host - let stakeAmount, slashMisses, slashPercentage + let collateralAmount, slashMisses, slashPercentage beforeEach(async function () { ;[client, host] = await ethers.getSigners() @@ -20,7 +20,7 @@ describe("Storage", function () { storage = await ethers.getContract("Storage") await token.mint(client.address, 1000) await token.mint(host.address, 1000) - stakeAmount = await storage.stakeAmount() + collateralAmount = await storage.collateralAmount() slashMisses = await storage.slashMisses() slashPercentage = await storage.slashPercentage() }) @@ -29,9 +29,9 @@ describe("Storage", function () { let id beforeEach(async function () { - await token.connect(host).approve(storage.address, stakeAmount) + await token.connect(host).approve(storage.address, collateralAmount) await token.connect(client).approve(storage.address, bid.price) - await storage.connect(host).increaseStake(stakeAmount) + await storage.connect(host).deposit(collateralAmount) let requestHash = hashRequest(request) let bidHash = hashBid({ ...bid, requestHash }) await storage.newContract( @@ -60,9 +60,9 @@ describe("Storage", function () { expect(await storage.host(id)).to.equal(await host.getAddress()) }) - it("locks up host stake", async function () { - await expect(storage.connect(host).withdrawStake()).to.be.revertedWith( - "Stake locked" + it("locks up host collateral", async function () { + await expect(storage.connect(host).withdraw()).to.be.revertedWith( + "Account locked" ) }) @@ -96,10 +96,10 @@ describe("Storage", function () { } } - it("unlocks the host stake", async function () { + it("unlocks the host collateral", async function () { await mineUntilEnd() await storage.finishContract(id) - await expect(storage.connect(host).withdrawStake()).not.to.be.reverted + await expect(storage.connect(host).withdraw()).not.to.be.reverted }) it("pays the host", async function () { @@ -137,21 +137,22 @@ describe("Storage", function () { await storage.markProofAsMissing(id, blocknumber) } - it("reduces stake when too many proofs are missing", async function () { + 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 expectedStake = (stakeAmount * (100 - slashPercentage)) / 100 - expect(await storage.stake(host.address)).to.equal(expectedStake) + const expectedBalance = + (collateralAmount * (100 - slashPercentage)) / 100 + expect(await storage.balanceOf(host.address)).to.equal(expectedBalance) }) }) }) - it("doesn't create contract with insufficient stake", async function () { - await token.connect(host).approve(storage.address, stakeAmount - 1) + 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).increaseStake(stakeAmount - 1) + await storage.connect(host).deposit(collateralAmount - 1) let requestHash = hashRequest(request) let bidHash = hashBid({ ...bid, requestHash }) await expect( @@ -168,13 +169,13 @@ describe("Storage", function () { await sign(client, requestHash), await sign(host, bidHash) ) - ).to.be.revertedWith("Insufficient stake") + ).to.be.revertedWith("Insufficient collateral") }) it("doesn't create contract without payment of price", async function () { - await token.connect(host).approve(storage.address, stakeAmount) + await token.connect(host).approve(storage.address, collateralAmount) await token.connect(client).approve(storage.address, bid.price - 1) - await storage.connect(host).increaseStake(stakeAmount) + await storage.connect(host).deposit(collateralAmount) let requestHash = hashRequest(request) let bidHash = hashBid({ ...bid, requestHash }) await expect(