Replace Stakes with Collateral
Removes the old Stakes implementation in favor of the new Collateral implementation.
This commit is contained in:
parent
1f01704afd
commit
e963a25c94
14
Readme.md
14
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
|
||||
-----
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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 })
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
})
|
||||
})
|
|
@ -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(
|
||||
|
|
Loading…
Reference in New Issue