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 An experimental implementation of the contracts that underly the Dagger storage
network. Its goal is to experiment with the rules around the bidding process, 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. completeness nor correctness are guaranteed at this moment in time.
Running 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 price of the contract. The payment is only released to the host upon successful
completion of the contract. completion of the contract.
Stakes Collateral
------ ------
To motivate a host to remain honest, it must put up some collateral (stake) To motivate a host to remain honest, it must put up some collateral before it is
before it is allowed to participate in storage contracts. The stake may not be allowed to participate in storage contracts. The collateral may not be withdrawn
withdrawn as long as a host is participating in an active storage contract. 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). percentage (slashed).
Proofs 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. 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 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 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 To Do
----- -----

View File

@ -5,7 +5,7 @@ import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "./AccountLocks.sol"; import "./AccountLocks.sol";
contract Collateral is AccountLocks { contract Collateral is AccountLocks {
IERC20 private immutable token; IERC20 public immutable token;
Totals private totals; Totals private totals;
mapping(address => uint256) private balances; mapping(address => uint256) private balances;

View File

@ -86,7 +86,7 @@ contract Contracts {
uint256 proofPeriod, uint256 proofPeriod,
uint256 proofTimeout, uint256 proofTimeout,
bytes32 nonce bytes32 nonce
) private pure returns (bytes32) { ) internal pure returns (bytes32) {
return return
keccak256( keccak256(
abi.encode( abi.encode(
@ -106,7 +106,7 @@ contract Contracts {
bytes32 requestHash, bytes32 requestHash,
uint256 expiry, uint256 expiry,
uint256 price uint256 price
) private pure returns (bytes32) { ) internal pure returns (bytes32) {
return keccak256(abi.encode("[dagger.bid.v1]", requestHash, expiry, price)); 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 "./Contracts.sol";
import "./Proofs.sol"; import "./Proofs.sol";
import "./Stakes.sol"; import "./Collateral.sol";
contract Storage is Contracts, Proofs, Stakes { contract Storage is Contracts, Proofs, Collateral {
uint256 public stakeAmount; uint256 public collateralAmount;
uint256 public slashMisses; uint256 public slashMisses;
uint256 public slashPercentage; uint256 public slashPercentage;
@ -14,11 +14,11 @@ contract Storage is Contracts, Proofs, Stakes {
constructor( constructor(
IERC20 token, IERC20 token,
uint256 _stakeAmount, uint256 _collateralAmount,
uint256 _slashMisses, uint256 _slashMisses,
uint256 _slashPercentage uint256 _slashPercentage
) Stakes(token) { ) Collateral(token) {
stakeAmount = _stakeAmount; collateralAmount = _collateralAmount;
slashMisses = _slashMisses; slashMisses = _slashMisses;
slashPercentage = _slashPercentage; slashPercentage = _slashPercentage;
} }
@ -36,9 +36,19 @@ contract Storage is Contracts, Proofs, Stakes {
bytes memory requestSignature, bytes memory requestSignature,
bytes memory bidSignature bytes memory bidSignature
) public { ) public {
require(_stake(_host) >= stakeAmount, "Insufficient stake"); require(balanceOf(_host) >= collateralAmount, "Insufficient collateral");
_lockStake(_host); bytes32 requestHash = _hashRequest(
_token().transferFrom(msg.sender, address(this), _price); _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( _newContract(
_duration, _duration,
_size, _size,
@ -66,9 +76,9 @@ contract Storage is Contracts, Proofs, Stakes {
function finishContract(bytes32 id) public { function finishContract(bytes32 id) public {
require(block.number > proofEnd(id), "Contract has not ended yet"); require(block.number > proofEnd(id), "Contract has not ended yet");
require(!finished[id], "Contract already finished"); require(!finished[id], "Contract already finished");
_unlockStake(host(id)); _unlock(id);
finished[id] = true; 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) { function duration(bytes32 contractId) public view returns (uint256) {
@ -107,10 +117,6 @@ contract Storage is Contracts, Proofs, Stakes {
return _missed(contractId); return _missed(contractId);
} }
function stake(address account) public view returns (uint256) {
return _stake(account);
}
function isProofRequired(bytes32 contractId, uint256 blocknumber) function isProofRequired(bytes32 contractId, uint256 blocknumber)
public public
view view
@ -141,12 +147,4 @@ contract Storage is Contracts, Proofs, Stakes {
_slash(host(contractId), slashPercentage); _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 }) => { module.exports = async ({ deployments, getNamedAccounts }) => {
const token = await deployments.get("TestToken") const token = await deployments.get("TestToken")
const stakeAmount = 100 const collateralAmount = 100
const slashMisses = 3 const slashMisses = 3
const slashPercentage = 10 const slashPercentage = 10
const args = [token.address, stakeAmount, slashMisses, slashPercentage] const args = [token.address, collateralAmount, slashMisses, slashPercentage]
const { deployer } = await getNamedAccounts() const { deployer } = await getNamedAccounts()
await deployments.deploy("Storage", { args, from: deployer }) 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 storage
let token let token
let client, host let client, host
let stakeAmount, slashMisses, slashPercentage let collateralAmount, slashMisses, slashPercentage
beforeEach(async function () { beforeEach(async function () {
;[client, host] = await ethers.getSigners() ;[client, host] = await ethers.getSigners()
@ -20,7 +20,7 @@ describe("Storage", function () {
storage = await ethers.getContract("Storage") storage = await ethers.getContract("Storage")
await token.mint(client.address, 1000) await token.mint(client.address, 1000)
await token.mint(host.address, 1000) await token.mint(host.address, 1000)
stakeAmount = await storage.stakeAmount() collateralAmount = await storage.collateralAmount()
slashMisses = await storage.slashMisses() slashMisses = await storage.slashMisses()
slashPercentage = await storage.slashPercentage() slashPercentage = await storage.slashPercentage()
}) })
@ -29,9 +29,9 @@ describe("Storage", function () {
let id let id
beforeEach(async function () { 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 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 requestHash = hashRequest(request)
let bidHash = hashBid({ ...bid, requestHash }) let bidHash = hashBid({ ...bid, requestHash })
await storage.newContract( await storage.newContract(
@ -60,9 +60,9 @@ describe("Storage", function () {
expect(await storage.host(id)).to.equal(await host.getAddress()) expect(await storage.host(id)).to.equal(await host.getAddress())
}) })
it("locks up host stake", async function () { it("locks up host collateral", async function () {
await expect(storage.connect(host).withdrawStake()).to.be.revertedWith( await expect(storage.connect(host).withdraw()).to.be.revertedWith(
"Stake locked" "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 mineUntilEnd()
await storage.finishContract(id) 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 () { it("pays the host", async function () {
@ -137,21 +137,22 @@ describe("Storage", function () {
await storage.markProofAsMissing(id, blocknumber) 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) await storage.connect(host).startContract(id)
for (let i = 0; i < slashMisses; i++) { for (let i = 0; i < slashMisses; i++) {
await ensureProofIsMissing() await ensureProofIsMissing()
} }
const expectedStake = (stakeAmount * (100 - slashPercentage)) / 100 const expectedBalance =
expect(await storage.stake(host.address)).to.equal(expectedStake) (collateralAmount * (100 - slashPercentage)) / 100
expect(await storage.balanceOf(host.address)).to.equal(expectedBalance)
}) })
}) })
}) })
it("doesn't create contract with insufficient stake", async function () { it("doesn't create contract with insufficient collateral", async function () {
await token.connect(host).approve(storage.address, stakeAmount - 1) await token.connect(host).approve(storage.address, collateralAmount - 1)
await token.connect(client).approve(storage.address, bid.price) 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 requestHash = hashRequest(request)
let bidHash = hashBid({ ...bid, requestHash }) let bidHash = hashBid({ ...bid, requestHash })
await expect( await expect(
@ -168,13 +169,13 @@ describe("Storage", function () {
await sign(client, requestHash), await sign(client, requestHash),
await sign(host, bidHash) 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 () { 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 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 requestHash = hashRequest(request)
let bidHash = hashBid({ ...bid, requestHash }) let bidHash = hashBid({ ...bid, requestHash })
await expect( await expect(