Replace Stakes with Collateral

Removes the old Stakes implementation in favor of the
new Collateral implementation.
This commit is contained in:
Mark Spanbroek 2022-02-15 17:54:19 +01:00 committed by markspanbroek
parent 1f01704afd
commit e963a25c94
9 changed files with 52 additions and 210 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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