mirror of
https://github.com/codex-storage/codex-contracts-eth.git
synced 2025-02-11 02:46:40 +00:00
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
|
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
|
||||||
-----
|
-----
|
||||||
|
@ -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;
|
||||||
|
|
||||||
|
@ -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));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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 "./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();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -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 }) => {
|
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 })
|
||||||
}
|
}
|
||||||
|
@ -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 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(
|
||||||
|
Loading…
x
Reference in New Issue
Block a user