Compare commits

..

No commits in common. "da20410f874c7907de252d14c342206941e3c33e" and "6b37cead66eed7844dafc185b9124427676e6615" have entirely different histories.

21 changed files with 473 additions and 776 deletions

View File

@ -1,105 +0,0 @@
// SPDX-License-Identifier: Unlicense
pragma solidity ^0.8.24;
struct Reference {
int weightPPM;
string targetPostId;
}
struct Author {
uint weightPPM;
address authorAddress;
}
struct Post {
string id;
address sender;
Author[] authors;
Reference[] references;
string content;
}
contract GlobalForum {
mapping(string => Post) posts;
string[] public postIds;
uint public postCount;
event PostAdded(string id);
function addPost(
Author[] calldata authors,
string calldata postId,
Reference[] calldata references
) external {
require(authors.length > 0, "Post must include at least one author");
postCount++;
postIds.push(postId);
Post storage post = posts[postId];
require(
post.authors.length == 0,
"A post with this postId already exists"
);
post.sender = msg.sender;
post.id = postId;
uint authorTotalWeightPPM;
for (uint i = 0; i < authors.length; i++) {
authorTotalWeightPPM += authors[i].weightPPM;
post.authors.push(authors[i]);
}
require(
authorTotalWeightPPM == 1000000,
"Author weights must sum to 1000000"
);
for (uint i = 0; i < references.length; i++) {
post.references.push(references[i]);
}
int totalReferenceWeightPos;
int totalReferenceWeightNeg;
for (uint i = 0; i < post.references.length; i++) {
int weight = post.references[i].weightPPM;
require(
weight >= -1000000,
"Each reference weight must be >= -1000000"
);
require(
weight <= 1000000,
"Each reference weight must be <= 1000000"
);
if (weight > 0) totalReferenceWeightPos += weight;
else totalReferenceWeightNeg += weight;
}
require(
totalReferenceWeightPos <= 1000000,
"Sum of positive references must be <= 1000000"
);
require(
totalReferenceWeightNeg >= -1000000,
"Sum of negative references must be >= -1000000"
);
emit PostAdded(postId);
}
function getPostAuthors(
string calldata postId
) external view returns (Author[] memory) {
Post storage post = posts[postId];
return post.authors;
}
function getPost(
string calldata postId
)
external
view
returns (
Author[] memory authors,
Reference[] memory references,
address sender
)
{
Post storage post = posts[postId];
authors = post.authors;
references = post.references;
sender = post.sender;
}
}

View File

@ -2,16 +2,16 @@
pragma solidity ^0.8.24;
import "./core/DAO.sol";
import "./core/Forum.sol";
import "./Work.sol";
import "./interfaces/IOnValidate.sol";
contract Onboarding is Work, IOnValidate {
constructor(
DAO dao,
GlobalForum forum,
Proposals proposals,
uint price
) Work(dao, forum, proposals, price) {}
DAO dao_,
Proposals proposals_,
uint price_
) Work(dao_, proposals_, price_) {}
/// Accept work approval/disapproval from customer
function submitWorkApproval(
@ -29,7 +29,7 @@ contract Onboarding is Work, IOnValidate {
// Make work evidence post
Author[] memory authors = new Author[](1);
authors[0] = Author(1000000, stake.worker);
forum.addPost(authors, request.evidencePostId, request.references);
dao.addPost(authors, request.evidencePostId, request.references);
emit WorkApprovalSubmitted(requestIndex, approval);
// Initiate validation pool
uint poolIndex = dao.initiateValidationPool{
@ -76,7 +76,7 @@ contract Onboarding is Work, IOnValidate {
Reference[] memory emptyReferences;
Author[] memory authors = new Author[](1);
authors[0] = Author(1000000, request.customer);
forum.addPost(authors, request.requestPostId, emptyReferences);
dao.addPost(authors, request.requestPostId, emptyReferences);
dao.initiateValidationPool{value: request.fee / 10}(
request.requestPostId,
POOL_DURATION,

View File

@ -59,7 +59,7 @@ contract Proposals is DAOContract, IOnValidate {
// TODO receive : we want to be able to accept refunds from validation pools
/// Submit a post as a proposal. forum.addPost should be called before this.
/// Submit a post as a proposal. DAO.addPost should be called before this.
function propose(
string calldata postId,
uint[3] calldata durations,

View File

@ -9,11 +9,10 @@ abstract contract RollableWork is Work {
constructor(
DAO dao,
GlobalForum forum,
Proposals proposalsContract,
Rollup rollupContract_,
uint price
) Work(dao, forum, proposalsContract, price) {
) Work(dao, proposalsContract, price) {
rollupContract = rollupContract_;
}
@ -35,7 +34,7 @@ abstract contract RollableWork is Work {
// Make work evidence post
Author[] memory authors = new Author[](1);
authors[0] = Author(1000000, stake.worker);
forum.addPost(authors, request.evidencePostId, request.references);
dao.addPost(authors, request.evidencePostId, request.references);
// send worker stakes and customer fee to rollup contract
dao.forwardAllowance(

View File

@ -2,6 +2,7 @@
pragma solidity ^0.8.24;
import "./core/DAO.sol";
import "./core/Forum.sol";
import "./Availability.sol";
import "./Proposals.sol";
import "./interfaces/IOnProposalAccepted.sol";
@ -30,7 +31,6 @@ abstract contract Work is Availability, IOnProposalAccepted {
uint proposalIndex;
}
GlobalForum forum;
Proposals proposalsContract;
uint public price;
mapping(uint => PriceProposal) public priceProposals;
@ -48,13 +48,11 @@ abstract contract Work is Availability, IOnProposalAccepted {
constructor(
DAO dao,
GlobalForum forum_,
Proposals proposalsContract_,
uint price_
) Availability(dao) {
price = price_;
proposalsContract = proposalsContract_;
forum = forum_;
}
/// Accept work request with fee
@ -109,7 +107,7 @@ abstract contract Work is Availability, IOnProposalAccepted {
// Make work evidence post
Author[] memory authors = new Author[](1);
authors[0] = Author(1000000, stake.worker);
forum.addPost(authors, request.evidencePostId, request.references);
dao.addPost(authors, request.evidencePostId, request.references);
emit WorkApprovalSubmitted(requestIndex, approval);
// Initiate validation pool
uint poolIndex = dao.initiateValidationPool{value: request.fee}(

View File

@ -7,9 +7,8 @@ import "./Proposals.sol";
contract Work1 is Work {
constructor(
DAO dao,
GlobalForum forum,
Proposals proposals,
uint price
) Work(dao, forum, proposals, price) {}
DAO dao_,
Proposals proposals_,
uint price_
) Work(dao_, proposals_, price_) {}
}

View File

@ -9,9 +9,8 @@ import "./Rollup.sol";
contract Work2 is RollableWork {
constructor(
DAO dao,
GlobalForum forum,
Proposals proposals,
Rollup rollup,
uint price
) RollableWork(dao, forum, proposals, rollup, price) {}
) RollableWork(dao, proposals, rollup, price) {}
}

View File

@ -2,7 +2,14 @@
pragma solidity ^0.8.24;
import "./DAO.sol";
import "../GlobalForum.sol";
import "./Forum.sol";
struct ValidationPoolStake {
uint id;
bool inFavor;
uint amount;
address sender;
}
struct ValidationPoolParams {
uint duration;
@ -21,48 +28,32 @@ struct ValidationPoolProps {
bool outcome;
}
struct ValidationPool {
uint id;
address sender;
mapping(uint => ValidationPoolStake) stakes;
uint stakeCount;
ValidationPoolParams params;
ValidationPoolProps props;
bool callbackOnValidate;
bytes callbackData;
}
contract Bench {
struct Stake {
uint id;
bool inFavor;
uint amount;
address sender;
}
struct Pool {
uint id;
address sender;
mapping(uint => Stake) stakes;
uint stakeCount;
ValidationPoolParams params;
ValidationPoolProps props;
bool callbackOnValidate;
bytes callbackData;
}
mapping(uint => Pool) public validationPools;
mapping(uint => ValidationPool) public validationPools;
uint public validationPoolCount;
DAO dao;
GlobalForum forum;
// Validation Pool parameters
uint constant minDuration = 1; // 1 second
uint constant maxDuration = 365_000_000 days; // 1 million years
uint[2] minQuorum = [1, 10];
// Forum parameters
// TODO: Make depth limit configurable; take as param
uint depthLimit = 3;
mapping(string => mapping(string => int)) _edgeBalances;
function registerDAO(DAO dao_, GlobalForum forum_) external {
function registerDAO(DAO dao_) external {
require(
address(dao) == address(0),
"A DAO has already been registered"
);
dao = dao_;
forum = forum_;
}
/// Register a stake for/against a validation pool
@ -76,14 +67,14 @@ contract Bench {
msg.sender == address(dao),
"Only DAO contract may call stakeOnValidationPool"
);
Pool storage pool = validationPools[poolIndex];
ValidationPool storage pool = validationPools[poolIndex];
require(
block.timestamp <= pool.props.endTime,
"Pool end time has passed"
);
// We don't call _update here; We defer that until evaluateOutcome.
uint stakeIndex = pool.stakeCount++;
Stake storage s = pool.stakes[stakeIndex];
ValidationPoolStake storage s = pool.stakes[stakeIndex];
s.sender = sender;
s.inFavor = inFavor;
s.amount = amount;
@ -116,7 +107,7 @@ contract Bench {
require(winRatio[0] <= winRatio[1], "Win ratio is greater than one");
require(bindingPercent <= 100, "Binding percent must be <= 100");
poolIndex = validationPoolCount++;
Pool storage pool = validationPools[poolIndex];
ValidationPool storage pool = validationPools[poolIndex];
pool.id = poolIndex;
pool.sender = sender;
pool.props.postId = postId;
@ -143,11 +134,11 @@ contract Bench {
msg.sender == address(dao),
"Only DAO contract may call evaluateOutcome"
);
Pool storage pool = validationPools[poolIndex];
ValidationPool storage pool = validationPools[poolIndex];
require(pool.props.resolved == false, "Pool is already resolved");
uint stakedFor;
uint stakedAgainst;
Stake storage s;
ValidationPoolStake storage s;
for (uint i = 0; i < pool.stakeCount; i++) {
s = pool.stakes[i];
// Make sure the sender still has the required balance.
@ -254,11 +245,9 @@ contract Bench {
}
// Transfer REP to the forum instead of to the author directly
propagateReputation(
dao.propagateReputation(
pool.props.postId,
int(pool.props.minted / 2 + remainder),
false,
0
int(pool.props.minted / 2 + remainder)
);
} else {
// If vote does not pass, divide the losing stake among the winners
@ -294,126 +283,4 @@ contract Bench {
);
}
}
function _handleReference(
string memory postId,
Reference memory ref,
int amount,
bool initialNegative,
uint depth
) internal returns (int outboundAmount) {
outboundAmount = (amount * ref.weightPPM) / 1000000;
if (bytes(ref.targetPostId).length == 0) {
// Incineration
require(
outboundAmount >= 0,
"Leaching from incinerator is forbidden"
);
dao.burn(address(dao), uint(outboundAmount));
return outboundAmount;
}
int balanceToOutbound = _edgeBalances[postId][ref.targetPostId];
if (initialNegative) {
if (outboundAmount < 0) {
outboundAmount = outboundAmount > -balanceToOutbound
? outboundAmount
: -balanceToOutbound;
} else {
outboundAmount = outboundAmount < -balanceToOutbound
? outboundAmount
: -balanceToOutbound;
}
}
int refund = propagateReputation(
ref.targetPostId,
outboundAmount,
initialNegative || (depth == 0 && ref.weightPPM < 0),
depth + 1
);
outboundAmount -= refund;
_edgeBalances[postId][ref.targetPostId] += outboundAmount;
}
function _distributeAmongAuthors(
Author[] memory authors,
int amount
) internal returns (int refund) {
int allocated;
for (uint i = 0; i < authors.length; i++) {
dao.registerMember(authors[i].authorAddress);
}
for (uint i = 0; i < authors.length; i++) {
Author memory author = authors[i];
int share;
if (i < authors.length - 1) {
share = (amount * int(author.weightPPM)) / 1000000;
allocated += share;
} else {
// For the last author, allocate the remainder.
share = amount - allocated;
}
if (share > 0) {
dao.update(address(dao), author.authorAddress, uint(share));
} else if (dao.balanceOf(author.authorAddress) < uint(-share)) {
// Author has already lost some REP gained from this post.
// That means other DAO members have earned it for policing.
// We need to refund the difference here to ensure accurate bookkeeping
uint authorBalance = dao.balanceOf(author.authorAddress);
refund += share + int(authorBalance);
dao.update(
author.authorAddress,
address(dao),
dao.balanceOf(author.authorAddress)
);
} else {
dao.update(author.authorAddress, address(dao), uint(-share));
}
}
}
function propagateReputation(
string memory postId,
int amount,
bool initialNegative,
uint depth
) internal returns (int refundToInbound) {
if (depth >= depthLimit) {
return amount;
}
Reference[] memory references;
Author[] memory authors;
address sender;
(authors, references, sender) = forum.getPost(postId);
if (authors.length == 0) {
// We most likely got here via a reference to a post that hasn't been added yet.
// We support this scenario so that a reference graph can be imported one post at a time.
return amount;
}
// Propagate negative references first
for (uint i = 0; i < references.length; i++) {
if (references[i].weightPPM < 0) {
amount -= _handleReference(
postId,
references[i],
amount,
initialNegative,
depth
);
}
}
// Now propagate positive references
for (uint i = 0; i < references.length; i++) {
if (references[i].weightPPM > 0) {
amount -= _handleReference(
postId,
references[i],
amount,
initialNegative,
depth
);
}
}
refundToInbound = _distributeAmongAuthors(authors, amount);
}
}

View File

@ -4,7 +4,7 @@ pragma solidity ^0.8.24;
import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "./Reputation.sol";
import "./Bench.sol";
import "../GlobalForum.sol";
import "./Forum.sol";
import "../interfaces/IAcceptAvailability.sol";
import "../interfaces/IOnValidate.sol";
@ -12,36 +12,27 @@ import "hardhat/console.sol";
contract DAO {
Reputation rep;
GlobalForum forum;
Forum forum;
Bench bench;
mapping(uint => address) public members;
uint public memberCount;
mapping(address => bool) public isMember;
event PostAdded(string id);
event ValidationPoolInitiated(uint poolIndex);
event ValidationPoolResolved(
uint poolIndex,
bool votePasses,
bool quorumMet
);
event LWValidationPoolInitiated(uint poolIndex);
event LWValidationPoolResolved(
uint poolIndex,
bool votePasses,
bool quorumMet
);
event PostAdded(string id);
constructor(Reputation reputation_, Bench bench_, GlobalForum forum_) {
constructor(Reputation reputation_, Forum forum_, Bench bench_) {
rep = reputation_;
bench = bench_;
forum = forum_;
bench = bench_;
rep.registerDAO(this);
bench.registerDAO(this, forum);
}
function emitPostAdded(string memory id) public {
emit PostAdded(id);
forum.registerDAO(this);
bench.registerDAO(this);
}
function emitValidationPoolInitiated(uint poolIndex) public {
@ -56,13 +47,13 @@ contract DAO {
emit ValidationPoolResolved(poolIndex, votePasses, quorumMet);
}
function emitLWValidationPoolInitiated(uint poolIndex) public {
emit LWValidationPoolInitiated(poolIndex);
function emitPostAdded(string memory id) public {
emit PostAdded(id);
}
function update(address from, address to, uint256 value) public {
require(
msg.sender == address(bench),
msg.sender == address(forum) || msg.sender == address(bench),
"Only DAO core contracts may call update"
);
rep.update(from, to, value);
@ -70,7 +61,7 @@ contract DAO {
function mint(address account, uint256 value) public {
require(
msg.sender == address(bench),
msg.sender == address(forum) || msg.sender == address(bench),
"Only DAO core contracts may call mint"
);
rep.mint(account, value);
@ -78,7 +69,7 @@ contract DAO {
function burn(address account, uint256 value) public {
require(
msg.sender == address(bench),
msg.sender == address(forum) || msg.sender == address(bench),
"Only DAO core contracts may call burn"
);
rep.burn(account, value);
@ -86,7 +77,7 @@ contract DAO {
function registerMember(address account) public {
require(
msg.sender == address(bench),
msg.sender == address(forum) || msg.sender == address(bench),
"Only DAO core contracts may call registerMember"
);
if (!isMember[account]) {
@ -138,6 +129,10 @@ contract DAO {
return true;
}
function propagateReputation(string memory postId, int amount) public {
forum.propagateReputation(postId, amount, false, 0);
}
function distributeFeeAmongMembers() public payable {
uint allocated;
for (uint i = 0; i < memberCount; i++) {
@ -241,7 +236,7 @@ contract DAO {
bytes calldata callbackData
) public {
require(
msg.sender == address(bench),
msg.sender == address(forum) || msg.sender == address(bench),
"Only DAO core contracts may call onValidate"
);
IOnValidate(target).onValidate(
@ -252,6 +247,34 @@ contract DAO {
callbackData
);
}
function addPost(
Author[] calldata authors,
string calldata postId,
Reference[] calldata references
) public {
forum.addPost(msg.sender, authors, postId, references);
}
function posts(
string calldata postId
) public view returns (string memory id, address sender, uint reputation) {
return forum.posts(postId);
}
function postCount() public view returns (uint) {
return forum.postCount();
}
function postIds(uint postIndex) public view returns (string memory) {
return forum.postIds(postIndex);
}
function getPostAuthors(
string calldata postId
) public view returns (Author[] memory) {
return forum.getPostAuthors(postId);
}
}
/// Convenience contract to extend for other contracts that will be initialized to

View File

@ -0,0 +1,251 @@
// SPDX-License-Identifier: Unlicense
pragma solidity ^0.8.24;
import "./DAO.sol";
struct Reference {
int weightPPM;
string targetPostId;
}
struct Author {
uint weightPPM;
address authorAddress;
}
struct Post {
string id;
address sender;
Author[] authors;
Reference[] references;
uint reputation;
// TODO: timestamp
}
contract Forum {
mapping(string => Post) public posts;
string[] public postIds;
uint public postCount;
mapping(string => mapping(string => int)) _edgeBalances;
DAO dao;
event PostAdded(string id);
// Forum parameters
// TODO: Make depth limit configurable; take as param in _onValidatePost callback
uint depthLimit = 3;
function registerDAO(DAO dao_) external {
require(
address(dao) == address(0),
"A DAO has already been registered"
);
dao = dao_;
}
function addPost(
address sender,
Author[] calldata authors,
string calldata postId,
Reference[] calldata references
) external {
require(
msg.sender == address(dao),
"Only DAO contract may call addPost"
);
require(authors.length > 0, "Post must include at least one author");
postCount++;
postIds.push(postId);
Post storage post = posts[postId];
require(
post.authors.length == 0,
"A post with this postId already exists"
);
post.sender = sender;
post.id = postId;
uint authorTotalWeightPPM;
for (uint i = 0; i < authors.length; i++) {
authorTotalWeightPPM += authors[i].weightPPM;
post.authors.push(authors[i]);
}
require(
authorTotalWeightPPM == 1000000,
"Author weights must sum to 1000000"
);
for (uint i = 0; i < references.length; i++) {
post.references.push(references[i]);
}
int totalReferenceWeightPos;
int totalReferenceWeightNeg;
for (uint i = 0; i < post.references.length; i++) {
int weight = post.references[i].weightPPM;
require(
weight >= -1000000,
"Each reference weight must be >= -1000000"
);
require(
weight <= 1000000,
"Each reference weight must be <= 1000000"
);
if (weight > 0) totalReferenceWeightPos += weight;
else totalReferenceWeightNeg += weight;
}
require(
totalReferenceWeightPos <= 1000000,
"Sum of positive references must be <= 1000000"
);
require(
totalReferenceWeightNeg >= -1000000,
"Sum of negative references must be >= -1000000"
);
dao.emitPostAdded(postId);
}
function getPostAuthors(
string calldata postId
) external view returns (Author[] memory) {
Post storage post = posts[postId];
return post.authors;
}
function _handleReference(
string memory postId,
Reference memory ref,
int amount,
bool initialNegative,
uint depth
) internal returns (int outboundAmount) {
outboundAmount = (amount * ref.weightPPM) / 1000000;
if (bytes(ref.targetPostId).length == 0) {
// Incineration
require(
outboundAmount >= 0,
"Leaching from incinerator is forbidden"
);
dao.burn(address(dao), uint(outboundAmount));
return outboundAmount;
}
int balanceToOutbound = _edgeBalances[postId][ref.targetPostId];
if (initialNegative) {
if (outboundAmount < 0) {
outboundAmount = outboundAmount > -balanceToOutbound
? outboundAmount
: -balanceToOutbound;
} else {
outboundAmount = outboundAmount < -balanceToOutbound
? outboundAmount
: -balanceToOutbound;
}
}
int refund = propagateReputation(
ref.targetPostId,
outboundAmount,
initialNegative || (depth == 0 && ref.weightPPM < 0),
depth + 1
);
outboundAmount -= refund;
_edgeBalances[postId][ref.targetPostId] += outboundAmount;
}
function _distributeAmongAuthors(
Post memory post,
int amount
) internal returns (int refund) {
int allocated;
for (uint i = 0; i < post.authors.length; i++) {
dao.registerMember(post.authors[i].authorAddress);
}
for (uint i = 0; i < post.authors.length; i++) {
Author memory author = post.authors[i];
int share;
if (i < post.authors.length - 1) {
share = (amount * int(author.weightPPM)) / 1000000;
allocated += share;
} else {
// For the last author, allocate the remainder.
share = amount - allocated;
}
if (share > 0) {
dao.update(address(dao), author.authorAddress, uint(share));
dao.registerMember(author.authorAddress);
} else if (dao.balanceOf(author.authorAddress) < uint(-share)) {
// Author has already lost some REP gained from this post.
// That means other DAO members have earned it for policing.
// We need to refund the difference here to ensure accurate bookkeeping
refund += share + int(dao.balanceOf(author.authorAddress));
dao.update(
author.authorAddress,
address(dao),
dao.balanceOf(author.authorAddress)
);
} else {
dao.update(author.authorAddress, address(dao), uint(-share));
}
}
}
function propagateReputation(
string memory postId,
int amount,
bool initialNegative,
uint depth
) public returns (int refundToInbound) {
require(
msg.sender == address(dao) || msg.sender == address(this),
"Only DAO contract may call propagateReputation"
);
if (depth >= depthLimit) {
return amount;
}
Post storage post = posts[postId];
if (post.authors.length == 0) {
// We most likely got here via a reference to a post that hasn't been added yet.
// We support this scenario so that a reference graph can be imported one post at a time.
return amount;
}
// Propagate negative references first
for (uint i = 0; i < post.references.length; i++) {
if (post.references[i].weightPPM < 0) {
amount -= _handleReference(
postId,
post.references[i],
amount,
initialNegative,
depth
);
}
}
// Now propagate positive references
for (uint i = 0; i < post.references.length; i++) {
if (post.references[i].weightPPM > 0) {
amount -= _handleReference(
postId,
post.references[i],
amount,
initialNegative,
depth
);
}
}
if (amount > 0) {
_distributeAmongAuthors(post, amount);
post.reputation += uint(amount);
} else {
if (int(post.reputation) + amount >= 0) {
// Reduce the reputation of each author proportionately;
// If any author has insufficient reputation, refund the difference.
refundToInbound = _distributeAmongAuthors(post, amount);
post.reputation -= uint(-amount);
} else {
// If we applied the full amount, the post's reputation would decrease below zero.
refundToInbound = int(post.reputation) + amount;
refundToInbound += _distributeAmongAuthors(
post,
-int(post.reputation)
);
post.reputation = 0;
}
}
}
}

View File

@ -1,370 +0,0 @@
// SPDX-License-Identifier: Unlicense
pragma solidity ^0.8.24;
import "./DAO.sol";
struct LWVPoolParams {
uint duration;
uint[2] quorum; // [ Numerator, Denominator ]
uint[2] winRatio; // [ Numerator, Denominator ]
uint bindingPercent;
bool redistributeLosingStakes;
}
struct LWVPoolProps {
string postId;
uint fee;
uint minted;
uint endTime;
bool resolved;
bool outcome;
}
contract LightweightBench {
struct Transfer {
address from;
address to;
uint amount;
}
struct ProposedResult {
Transfer[] transfers;
uint stakedFor;
}
struct Stake {
uint id;
bool inFavor;
uint amount;
address sender;
string resultHash;
}
struct Pool {
uint id;
address sender;
mapping(string => ProposedResult) proposedResults;
string[] proposedResultHashes;
mapping(uint => Stake) stakes;
uint stakeCount;
LWVPoolParams params;
LWVPoolProps props;
bool callbackOnValidate;
bytes callbackData;
}
mapping(uint => Pool) public validationPools;
uint public validationPoolCount;
DAO dao;
uint constant minDuration = 1; // 1 second
uint constant maxDuration = 365_000_000 days; // 1 million years
uint[2] minQuorum = [1, 10];
function registerDAO(DAO dao_) external {
require(
address(dao) == address(0),
"A DAO has already been registered"
);
dao = dao_;
}
/// Accept fee to initiate a validation pool
function initiateValidationPool(
address sender,
string calldata postId,
uint duration,
uint[2] calldata quorum, // [Numerator, Denominator]
uint[2] calldata winRatio, // [Numerator, Denominator]
uint bindingPercent,
bool redistributeLosingStakes,
bool callbackOnValidate,
bytes calldata callbackData
) external payable returns (uint poolIndex) {
require(
msg.sender == address(dao),
"Only DAO contract may call initiateValidationPool"
);
require(duration >= minDuration, "Duration is too short");
require(duration <= maxDuration, "Duration is too long");
require(
minQuorum[1] * quorum[0] >= minQuorum[0] * quorum[1],
"Quorum is below minimum"
);
require(quorum[0] <= quorum[1], "Quorum is greater than one");
require(winRatio[0] <= winRatio[1], "Win ratio is greater than one");
require(bindingPercent <= 100, "Binding percent must be <= 100");
poolIndex = validationPoolCount++;
Pool storage pool = validationPools[poolIndex];
pool.id = poolIndex;
pool.sender = sender;
pool.props.postId = postId;
pool.props.fee = msg.value;
pool.props.endTime = block.timestamp + duration;
pool.params.quorum = quorum;
pool.params.winRatio = winRatio;
pool.params.bindingPercent = bindingPercent;
pool.params.redistributeLosingStakes = redistributeLosingStakes;
pool.params.duration = duration;
pool.callbackOnValidate = callbackOnValidate;
pool.callbackData = callbackData;
// We use our privilege as the DAO contract to mint reputation in proportion with the fee.
// Here we assume a minting ratio of 1
// TODO: Make minting ratio an adjustable parameter
dao.mint(address(dao), pool.props.fee);
pool.props.minted = msg.value;
dao.emitLWValidationPoolInitiated(poolIndex);
}
function proposeResult(
uint poolIndex,
string calldata resultHash,
Transfer[] calldata transfers
) external {
Pool storage pool = validationPools[poolIndex];
require(
block.timestamp <= pool.props.endTime,
"Pool end time has passed"
);
ProposedResult storage proposedResult = pool.proposedResults[
resultHash
];
pool.proposedResultHashes.push(resultHash);
require(
proposedResult.transfers.length == 0,
"This result hash has already been proposed"
);
for (uint i = 0; i < transfers.length; i++) {
proposedResult.transfers.push(transfers[i]);
}
}
/// Register a stake for/against a validation pool
function stakeOnValidationPool(
uint poolIndex,
string calldata resultHash,
address sender,
uint256 amount,
bool inFavor
) external {
require(
msg.sender == address(dao),
"Only DAO contract may call stakeOnValidationPool"
);
Pool storage pool = validationPools[poolIndex];
require(
block.timestamp <= pool.props.endTime,
"Pool end time has passed"
);
if (inFavor) {
ProposedResult storage proposedResult = pool.proposedResults[
resultHash
];
require(
proposedResult.transfers.length > 0,
"This result hash has not been proposed"
);
}
// We don't call _update here; We defer that until evaluateOutcome.
uint stakeIndex = pool.stakeCount++;
Stake storage s = pool.stakes[stakeIndex];
s.sender = sender;
s.inFavor = inFavor;
s.amount = amount;
s.id = stakeIndex;
s.resultHash = resultHash;
}
/// Evaluate outcome of a validation pool
function evaluateOutcome(uint poolIndex) public returns (bool votePasses) {
require(
msg.sender == address(dao),
"Only DAO contract may call evaluateOutcome"
);
Pool storage pool = validationPools[poolIndex];
require(pool.props.resolved == false, "Pool is already resolved");
uint stakedFor;
uint stakedAgainst;
Stake storage s;
for (uint i = 0; i < pool.stakeCount; i++) {
s = pool.stakes[i];
// Make sure the sender still has the required balance.
// If not, automatically decrease the staked amount.
if (dao.balanceOf(s.sender) < s.amount) {
s.amount = dao.balanceOf(s.sender);
}
if (s.inFavor) {
ProposedResult storage proposedResult = pool.proposedResults[
s.resultHash
];
proposedResult.stakedFor += s.amount;
} else {
stakedAgainst += s.amount;
}
}
// Determine the winning result hash
uint[] memory stakedForResult = new uint[](
pool.proposedResultHashes.length
);
uint winningResult;
for (uint i = 0; i < pool.proposedResultHashes.length; i++) {
string storage proposedResultHash = pool.proposedResultHashes[i];
ProposedResult storage proposedResult = pool.proposedResults[
proposedResultHash
];
stakedForResult[i] += proposedResult.stakedFor;
if (stakedForResult[i] > stakedForResult[winningResult]) {
winningResult = i;
}
}
// Only count stakes for the winning hash among the total staked in favor of the pool
for (uint i = 0; i < pool.stakeCount; i++) {
s = pool.stakes[i];
if (
s.inFavor &&
keccak256(bytes(s.resultHash)) ==
keccak256(bytes(pool.proposedResultHashes[winningResult]))
) {
stakedFor += s.amount;
}
}
stakedFor += pool.props.minted / 2;
stakedAgainst += pool.props.minted / 2;
if (pool.props.minted % 2 != 0) {
stakedFor += 1;
}
// Special case for early evaluation if dao.totalSupply has been staked
require(
block.timestamp > pool.props.endTime ||
stakedFor + stakedAgainst == dao.totalSupply(),
"Pool end time has not yet arrived"
);
// Check that quorum is met
if (
pool.params.quorum[1] * (stakedFor + stakedAgainst) <=
dao.totalSupply() * pool.params.quorum[0]
) {
// TODO: Refund fee
// TODO: this could be made available for the sender to withdraw
// payable(pool.sender).transfer(pool.props.fee);
pool.props.resolved = true;
dao.emitValidationPoolResolved(poolIndex, false, false);
// Callback if requested
if (pool.callbackOnValidate) {
dao.onValidate(
pool.sender,
votePasses,
false,
stakedFor,
stakedAgainst,
pool.callbackData
);
}
return false;
}
// A tie is resolved in favor of the validation pool.
// This is especially important so that the DAO's first pool can pass,
// when no reputation has yet been minted.
votePasses =
stakedFor * pool.params.winRatio[1] >=
(stakedFor + stakedAgainst) * pool.params.winRatio[0];
pool.props.resolved = true;
pool.props.outcome = votePasses;
dao.emitValidationPoolResolved(poolIndex, votePasses, true);
// Value of losing stakes should be distributed among winners, in proportion to their stakes
// Only bindingPercent % should be redistributed
// Stake senders should get (1000000-bindingPercent) % back
uint amountFromWinners = votePasses ? stakedFor : stakedAgainst;
uint totalRewards;
uint totalAllocated;
for (uint i = 0; i < pool.stakeCount; i++) {
s = pool.stakes[i];
if (votePasses != s.inFavor) {
// Losing stake
uint amount = (s.amount * pool.params.bindingPercent) / 100;
if (pool.params.redistributeLosingStakes) {
dao.update(s.sender, address(dao), amount);
totalRewards += amount;
} else {
dao.burn(s.sender, amount);
}
}
}
if (votePasses) {
// If vote passes, reward the author as though they had staked the winning portion of the VP initial stake
// Here we assume a stakeForAuthor ratio of 0.5
// TODO: Make stakeForAuthor an adjustable parameter
totalRewards += pool.props.minted / 2;
// Include the losign portion of the VP initial stake
// Issue rewards to the winners
for (uint i = 0; i < pool.stakeCount; i++) {
s = pool.stakes[i];
if (
pool.params.redistributeLosingStakes &&
votePasses == s.inFavor
) {
// Winning stake
uint reward = (((totalRewards * s.amount) /
amountFromWinners) * pool.params.bindingPercent) / 100;
totalAllocated += reward;
dao.update(address(dao), s.sender, reward);
}
}
// Due to rounding, there may be some excess REP. Award it to the author.
uint remainder = totalRewards - totalAllocated;
if (pool.props.minted % 2 != 0) {
// We staked the odd remainder in favor of the post, on behalf of the author.
remainder += 1;
}
// Execute the transfers from the winning proposed result
ProposedResult storage result = pool.proposedResults[
pool.proposedResultHashes[winningResult]
];
for (uint i = 0; i < result.transfers.length; i++) {
dao.update(
result.transfers[i].from,
result.transfers[i].to,
result.transfers[i].amount
);
}
} else {
// If vote does not pass, divide the losing stake among the winners
totalRewards += pool.props.minted;
for (uint i = 0; i < pool.stakeCount; i++) {
s = pool.stakes[i];
if (
pool.params.redistributeLosingStakes &&
votePasses == s.inFavor
) {
// Winning stake
uint reward = (((totalRewards * s.amount) /
(amountFromWinners - pool.props.minted / 2)) *
pool.params.bindingPercent) / 100;
totalAllocated += reward;
dao.update(address(dao), s.sender, reward);
}
}
}
// Distribute fee proportionately among all reputation holders
dao.distributeFeeAmongMembers{value: pool.props.fee}();
// Callback if requested
if (pool.callbackOnValidate) {
dao.onValidate(
pool.sender,
votePasses,
true,
stakedFor,
stakedAgainst,
pool.callbackData
);
}
}
}

View File

@ -1,7 +1,7 @@
const deployDAOCoreContracts = require('./util/deploy-core-contracts');
const deployCoreContracts = require('./util/deploy-core-contracts');
async function main() {
await deployDAOCoreContracts();
await deployCoreContracts();
}
main().catch((error) => {

View File

@ -1,15 +1,13 @@
require('dotenv').config();
const deployContract = require('./util/deploy-contract');
const deployDAOContract = require('./util/deploy-dao-contract');
const deployWorkContract = require('./util/deploy-work-contract');
const deployRollableWorkContract = require('./util/deploy-rollable-work-contract');
const deployDAOCoreContracts = require('./util/deploy-core-contracts');
const deployCoreContracts = require('./util/deploy-core-contracts');
const { ROLLUP_INTERVAL } = process.env;
async function main() {
await deployContract('GlobalForum');
await deployDAOCoreContracts();
await deployCoreContracts();
await deployDAOContract('Rollup', [ROLLUP_INTERVAL]);
await deployDAOContract('Proposals');
await deployWorkContract('Work1');

View File

@ -4,14 +4,15 @@ const contractAddresses = require('../../contract-addresses.json');
const network = process.env.HARDHAT_NETWORK;
const deployDAOCoreContracts = async () => {
const deployCoreContracts = async () => {
await deployContract('Reputation', [], true);
await deployContract('Forum', [], true);
await deployContract('Bench', [], true);
await deployContract('DAO', [
contractAddresses[network].Reputation,
contractAddresses[network].GlobalForum,
contractAddresses[network].Forum,
contractAddresses[network].Bench,
], true);
};
module.exports = deployDAOCoreContracts;
module.exports = deployCoreContracts;

View File

@ -13,7 +13,6 @@ const deployWorkContract = async (name) => {
await deployContract(name, [
contractAddresses[network].DAO,
contractAddresses[network].GlobalForum,
contractAddresses[network].Proposals,
price]);
};

View File

@ -8,18 +8,16 @@ const deployDAO = require('./util/deploy-dao');
describe('Forum', () => {
async function deploy() {
const [account1, account2, account3, account4] = await ethers.getSigners();
const { dao, forum } = await deployDAO();
const [account1, account2, account3] = await ethers.getSigners();
const { dao } = await deployDAO();
return {
dao, forum, account1, account2, account3, account4,
dao, account1, account2, account3,
};
}
let dao;
let forum;
let account1;
let account2;
let account3;
let account4;
const POOL_DURATION = 3600; // 1 hour
const POOL_FEE = 100;
const emptyCallbackData = ethers.AbiCoder.defaultAbiCoder().encode([], []);
@ -41,7 +39,7 @@ describe('Forum', () => {
{ value: fee ?? POOL_FEE },
);
const addPost = (author, postId, references) => forum.addPost([{
const addPost = (author, postId, references) => dao.addPost([{
weightPPM: 1000000,
authorAddress: author,
}], postId, references);
@ -49,43 +47,49 @@ describe('Forum', () => {
describe('Post', () => {
beforeEach(async () => {
({
dao, forum, account1, account2, account3, account4,
dao, account1, account2, account3,
} = await loadFixture(deploy));
});
it('should be able to add a post', async () => {
const postId = 'some-id';
await expect(addPost(account1, postId, [])).to.emit(forum, 'PostAdded').withArgs('some-id');
const post = await forum.getPost(postId);
await expect(addPost(account1, postId, [])).to.emit(dao, 'PostAdded').withArgs('some-id');
const post = await dao.posts(postId);
expect(post.sender).to.equal(account1);
expect(post.authors).to.have.length(1);
expect(post.authors[0].weightPPM).to.equal(1000000);
expect(post.authors[0].authorAddress).to.equal(account1);
expect(post.id).to.equal(postId);
const postAuthors = await dao.getPostAuthors(postId);
expect(postAuthors).to.have.length(1);
expect(postAuthors[0].weightPPM).to.equal(1000000);
expect(postAuthors[0].authorAddress).to.equal(account1);
});
it('should be able to add a post on behalf of another account', async () => {
const postId = 'some-id';
await addPost(account2, postId, []);
const post = await forum.getPost(postId);
const post = await dao.posts(postId);
expect(post.sender).to.equal(account1);
expect(post.authors).to.have.length(1);
expect(post.authors[0].weightPPM).to.equal(1000000);
expect(post.authors[0].authorAddress).to.equal(account2);
expect(post.id).to.equal(postId);
const postAuthors = await dao.getPostAuthors(postId);
expect(postAuthors).to.have.length(1);
expect(postAuthors[0].weightPPM).to.equal(1000000);
expect(postAuthors[0].authorAddress).to.equal(account2);
});
it('should be able to add a post with multiple authors', async () => {
const postId = 'some-id';
await expect(forum.addPost([
await expect(dao.addPost([
{ weightPPM: 500000, authorAddress: account1 },
{ weightPPM: 500000, authorAddress: account2 },
], postId, [])).to.emit(forum, 'PostAdded').withArgs('some-id');
const post = await forum.getPost(postId);
], postId, [])).to.emit(dao, 'PostAdded').withArgs('some-id');
const post = await dao.posts(postId);
expect(post.sender).to.equal(account1);
expect(post.authors).to.have.length(2);
expect(post.authors[0].weightPPM).to.equal(500000);
expect(post.authors[0].authorAddress).to.equal(account1);
expect(post.authors[1].weightPPM).to.equal(500000);
expect(post.authors[1].authorAddress).to.equal(account2);
expect(post.id).to.equal(postId);
const postAuthors = await dao.getPostAuthors(postId);
expect(postAuthors).to.have.length(2);
expect(postAuthors[0].weightPPM).to.equal(500000);
expect(postAuthors[0].authorAddress).to.equal(account1);
expect(postAuthors[1].weightPPM).to.equal(500000);
expect(postAuthors[1].authorAddress).to.equal(account2);
await initiateValidationPool({ postId: 'some-id' });
await time.increase(POOL_DURATION + 1);
await dao.evaluateOutcome(0);
@ -95,7 +99,7 @@ describe('Forum', () => {
it('should not be able to add a post with total author weight < 100%', async () => {
const postId = 'some-id';
await expect(forum.addPost([
await expect(dao.addPost([
{ weightPPM: 500000, authorAddress: account1 },
{ weightPPM: 400000, authorAddress: account2 },
], postId, [])).to.be.rejectedWith('Author weights must sum to 1000000');
@ -103,7 +107,7 @@ describe('Forum', () => {
it('should not be able to add a post with total author weight > 100%', async () => {
const postId = 'some-id';
await expect(forum.addPost([
await expect(dao.addPost([
{ weightPPM: 500000, authorAddress: account1 },
{ weightPPM: 600000, authorAddress: account2 },
], postId, [])).to.be.rejectedWith('Author weights must sum to 1000000');
@ -122,10 +126,13 @@ describe('Forum', () => {
it('should be able to leach reputation via references', async () => {
await addPost(account1, 'content-id', []);
expect((await dao.posts('content-id')).reputation).to.equal(0);
await initiateValidationPool({ postId: 'content-id' });
await dao.evaluateOutcome(0);
expect(await dao.balanceOf(account1)).to.equal(100);
expect((await dao.posts('content-id')).reputation).to.equal(100);
await addPost(account2, 'second-content-id', [{ weightPPM: -500000, targetPostId: 'content-id' }]);
expect((await dao.posts('second-content-id')).reputation).to.equal(0);
await initiateValidationPool({ postId: 'second-content-id' });
const pool = await dao.validationPools(1);
expect(pool.props.postId).to.equal('second-content-id');
@ -133,6 +140,8 @@ describe('Forum', () => {
await dao.evaluateOutcome(1);
expect(await dao.balanceOf(account1)).to.equal(50);
expect(await dao.balanceOf(account2)).to.equal(150);
expect((await dao.posts('content-id')).reputation).to.equal(50);
expect((await dao.posts('second-content-id')).reputation).to.equal(150);
});
it('should be able to redistribute power via references', async () => {
@ -212,6 +221,7 @@ describe('Forum', () => {
});
it('should limit effects of negative references on prior positive references', async () => {
console.log('First post');
await addPost(account1, 'content-id', []);
await initiateValidationPool({ postId: 'content-id' });
await dao.evaluateOutcome(0);
@ -253,15 +263,21 @@ describe('Forum', () => {
it('should enforce depth limit', async () => {
await addPost(account1, 'content-id-1', []);
await addPost(account2, 'content-id-2', [{ weightPPM: 1000000, targetPostId: 'content-id-1' }]);
await addPost(account3, 'content-id-3', [{ weightPPM: 1000000, targetPostId: 'content-id-2' }]);
await addPost(account4, 'content-id-4', [{ weightPPM: 1000000, targetPostId: 'content-id-3' }]);
await addPost(account1, 'content-id-2', [{ weightPPM: 1000000, targetPostId: 'content-id-1' }]);
await addPost(account1, 'content-id-3', [{ weightPPM: 1000000, targetPostId: 'content-id-2' }]);
await addPost(account1, 'content-id-4', [{ weightPPM: 1000000, targetPostId: 'content-id-3' }]);
await initiateValidationPool({ postId: 'content-id-4' });
await dao.evaluateOutcome(0);
expect(await dao.balanceOf(account1)).to.equal(0);
expect(await dao.balanceOf(account2)).to.equal(100);
expect(await dao.balanceOf(account3)).to.equal(0);
expect(await dao.balanceOf(account4)).to.equal(0);
const posts = await Promise.all([
await dao.posts('content-id-1'),
await dao.posts('content-id-2'),
await dao.posts('content-id-3'),
await dao.posts('content-id-4'),
]);
expect(posts[0].reputation).to.equal(0);
expect(posts[1].reputation).to.equal(100);
expect(posts[2].reputation).to.equal(0);
expect(posts[3].reputation).to.equal(0);
});
it('should be able to incinerate reputation', async () => {
@ -274,6 +290,7 @@ describe('Forum', () => {
await initiateValidationPool({ postId: 'content-id-1' });
expect(await dao.totalSupply()).to.equal(100);
await dao.evaluateOutcome(0);
expect((await dao.posts('content-id-1')).reputation).to.equal(50);
expect(await dao.totalSupply()).to.equal(50);
});
@ -284,6 +301,7 @@ describe('Forum', () => {
await dao.evaluateOutcome(0);
expect(await dao.balanceOf(account1)).to.equal(100);
expect(await dao.totalSupply()).to.equal(100);
expect((await dao.posts('content-id')).reputation).to.equal(100);
await addPost(account2, 'second-content-id', []);
await initiateValidationPool({ postId: 'second-content-id' });
@ -292,6 +310,8 @@ describe('Forum', () => {
expect(await dao.balanceOf(account1)).to.equal(100);
expect(await dao.balanceOf(account2)).to.equal(100);
expect(await dao.totalSupply()).to.equal(200);
expect((await dao.posts('content-id')).reputation).to.equal(100);
expect((await dao.posts('second-content-id')).reputation).to.equal(100);
// account1 stakes and loses
await initiateValidationPool({ postId: 'second-content-id' });
@ -302,6 +322,8 @@ describe('Forum', () => {
expect(await dao.balanceOf(account1)).to.equal(50);
expect(await dao.balanceOf(account2)).to.equal(250);
expect(await dao.totalSupply()).to.equal(300);
expect((await dao.posts('content-id')).reputation).to.equal(100);
expect((await dao.posts('second-content-id')).reputation).to.equal(100);
});
it('author and post rep can be completely destroyed', async () => {
@ -314,6 +336,9 @@ describe('Forum', () => {
expect(await dao.balanceOf(account2)).to.equal(250);
expect(await dao.balanceOf(account3)).to.equal(250);
expect(await dao.totalSupply()).to.equal(500);
expect((await dao.posts('content-id')).reputation).to.equal(0);
expect((await dao.posts('second-content-id')).reputation).to.equal(100);
expect((await dao.posts('third-content-id')).reputation).to.equal(250);
});
it('author rep can be destroyed while some post rep remains', async () => {
@ -326,6 +351,9 @@ describe('Forum', () => {
expect(await dao.balanceOf(account1)).to.equal(0);
expect(await dao.balanceOf(account2)).to.equal(250);
expect(await dao.balanceOf(account3)).to.equal(120);
expect((await dao.posts('content-id')).reputation).to.equal(30);
expect((await dao.posts('second-content-id')).reputation).to.equal(100);
expect((await dao.posts('third-content-id')).reputation).to.equal(120);
});
it('author rep can be destroyed while some post rep remains (odd amount)', async () => {
@ -338,12 +366,15 @@ describe('Forum', () => {
expect(await dao.balanceOf(account1)).to.equal(0);
expect(await dao.balanceOf(account2)).to.equal(250);
expect(await dao.balanceOf(account3)).to.equal(125);
expect((await dao.posts('content-id')).reputation).to.equal(25);
expect((await dao.posts('second-content-id')).reputation).to.equal(100);
expect((await dao.posts('third-content-id')).reputation).to.equal(125);
});
});
describe('negative reference of a post with multiple authors', async () => {
beforeEach(async () => {
await forum.addPost([
await dao.addPost([
{ weightPPM: 500000, authorAddress: account1 },
{ weightPPM: 500000, authorAddress: account2 },
], 'content-id', []);
@ -352,6 +383,7 @@ describe('Forum', () => {
expect(await dao.balanceOf(account1)).to.equal(50);
expect(await dao.balanceOf(account2)).to.equal(50);
expect(await dao.totalSupply()).to.equal(100);
expect((await dao.posts('content-id')).reputation).to.equal(100);
// account1 stakes and loses
await initiateValidationPool({ postId: 'content-id' });
@ -362,6 +394,7 @@ describe('Forum', () => {
expect(await dao.balanceOf(account1)).to.equal(25);
expect(await dao.balanceOf(account2)).to.equal(175);
expect(await dao.totalSupply()).to.equal(200);
expect((await dao.posts('content-id')).reputation).to.equal(100);
});
it('author and post rep can be completely destroyed', async () => {
@ -371,9 +404,11 @@ describe('Forum', () => {
await time.increase(POOL_DURATION + 1);
await dao.evaluateOutcome(2);
expect(await dao.balanceOf(account1)).to.equal(0);
expect(await dao.balanceOf(account2)).to.equal(0);
expect(await dao.balanceOf(account3)).to.equal(600);
expect(await dao.balanceOf(account2)).to.equal(125);
expect(await dao.balanceOf(account3)).to.equal(475);
expect(await dao.totalSupply()).to.equal(600);
expect((await dao.posts('content-id')).reputation).to.equal(0);
expect((await dao.posts('second-content-id')).reputation).to.equal(475);
});
it('author rep can be destroyed while some post rep remains', async () => {
@ -386,6 +421,8 @@ describe('Forum', () => {
expect(await dao.balanceOf(account1)).to.equal(0);
expect(await dao.balanceOf(account2)).to.equal(140);
expect(await dao.balanceOf(account3)).to.equal(130);
expect((await dao.posts('content-id')).reputation).to.equal(30);
expect((await dao.posts('second-content-id')).reputation).to.equal(130);
});
});
});

View File

@ -13,13 +13,13 @@ describe('Onboarding', () => {
// Contracts are deployed using the first signer/account by default
const [account1, account2] = await ethers.getSigners();
const { dao, forum } = await deployDAO();
const { dao } = await deployDAO();
const Proposals = await ethers.getContractFactory('Proposals');
const proposals = await Proposals.deploy(dao.target);
const Onboarding = await ethers.getContractFactory('Onboarding');
const onboarding = await Onboarding.deploy(dao.target, forum.target, proposals.target, PRICE);
const onboarding = await Onboarding.deploy(dao.target, proposals.target, PRICE);
await forum.addPost([{ weightPPM: 1000000, authorAddress: account1 }], 'content-id', []);
await dao.addPost([{ weightPPM: 1000000, authorAddress: account1 }], 'content-id', []);
const callbackData = ethers.AbiCoder.defaultAbiCoder().encode([], []);
await dao.initiateValidationPool(
'content-id',
@ -37,7 +37,7 @@ describe('Onboarding', () => {
expect(await dao.balanceOf(account1)).to.equal(100);
return {
dao, forum, onboarding, account1, account2,
dao, onboarding, account1, account2,
};
}
@ -53,14 +53,13 @@ describe('Onboarding', () => {
describe('Work approval/disapproval', () => {
let dao;
let forum;
let onboarding;
let account1;
let account2;
beforeEach(async () => {
({
dao, forum, onboarding, account1, account2,
dao, onboarding, account1, account2,
} = await loadFixture(deploy));
await dao.stakeAvailability(onboarding.target, 50, STAKE_DURATION);
});
@ -71,11 +70,13 @@ describe('Onboarding', () => {
await expect(onboarding.submitWorkApproval(0, true))
.to.emit(dao, 'ValidationPoolInitiated').withArgs(1)
.to.emit(onboarding, 'WorkApprovalSubmitted').withArgs(0, true);
const post = await forum.getPost('evidence-content-id');
const post = await dao.posts('evidence-content-id');
expect(post.sender).to.equal(onboarding.target);
expect(post.authors).to.have.length(1);
expect(post.authors[0].weightPPM).to.equal(1000000);
expect(post.authors[0].authorAddress).to.equal(account1);
expect(post.id).to.equal('evidence-content-id');
const postAuthors = await dao.getPostAuthors('evidence-content-id');
expect(postAuthors).to.have.length(1);
expect(postAuthors[0].weightPPM).to.equal(1000000);
expect(postAuthors[0].authorAddress).to.equal(account1);
const pool = await dao.validationPools(1);
expect(pool.props.postId).to.equal('evidence-content-id');
expect(pool.props.fee).to.equal(PRICE * 0.9);
@ -113,7 +114,7 @@ describe('Onboarding', () => {
describe('Onboarding followup', () => {
it('resolving the first validation pool should trigger a second pool', async () => {
const {
dao, forum, onboarding, account2,
dao, onboarding, account2,
} = await loadFixture(deploy);
await dao.stakeAvailability(onboarding.target, 50, STAKE_DURATION);
await onboarding.connect(account2).requestWork('req-content-id', { value: PRICE });
@ -121,12 +122,14 @@ describe('Onboarding', () => {
await expect(onboarding.submitWorkApproval(0, true)).to.emit(dao, 'ValidationPoolInitiated').withArgs(1);
await time.increase(86401);
await expect(dao.evaluateOutcome(1)).to.emit(dao, 'ValidationPoolInitiated').withArgs(2);
expect(await forum.postCount()).to.equal(3);
const post = await forum.getPost('req-content-id');
expect(await dao.postCount()).to.equal(3);
const post = await dao.posts('req-content-id');
expect(post.sender).to.equal(onboarding.target);
expect(post.authors).to.have.length(1);
expect(post.authors[0].weightPPM).to.equal(1000000);
expect(post.authors[0].authorAddress).to.equal(account2);
expect(post.id).to.equal('req-content-id');
const postAuthors = await dao.getPostAuthors('req-content-id');
expect(postAuthors).to.have.length(1);
expect(postAuthors[0].weightPPM).to.equal(1000000);
expect(postAuthors[0].authorAddress).to.equal(account2);
const pool = await dao.validationPools(2);
expect(pool.props.postId).to.equal('req-content-id');
expect(pool.props.fee).to.equal(PRICE * 0.1);
@ -136,7 +139,7 @@ describe('Onboarding', () => {
it('if the first validation pool is rejected it should not trigger a second pool', async () => {
const {
dao, forum, onboarding, account2,
dao, onboarding, account2,
} = await loadFixture(deploy);
await dao.stakeAvailability(onboarding.target, 40, STAKE_DURATION);
await onboarding.connect(account2).requestWork('req-content-id', { value: PRICE });
@ -145,7 +148,7 @@ describe('Onboarding', () => {
await dao.stakeOnValidationPool(1, 60, false);
await time.increase(86401);
await expect(dao.evaluateOutcome(1)).not.to.emit(dao, 'ValidationPoolInitiated');
expect(await forum.postCount()).to.equal(2);
expect(await dao.postCount()).to.equal(2);
});
});
});

View File

@ -12,12 +12,12 @@ describe('Proposal', () => {
// Contracts are deployed using the first signer/account by default
const [account1, account2] = await ethers.getSigners();
const { dao, forum } = await deployDAO();
const { dao } = await deployDAO();
const Proposals = await ethers.getContractFactory('Proposals');
const proposals = await Proposals.deploy(dao.target);
await forum.addPost([{ weightPPM: 1000000, authorAddress: account1 }], 'some-content-id', []);
await forum.addPost([{ weightPPM: 1000000, authorAddress: account2 }], 'some-other-content-id', []);
await dao.addPost([{ weightPPM: 1000000, authorAddress: account1 }], 'some-content-id', []);
await dao.addPost([{ weightPPM: 1000000, authorAddress: account2 }], 'some-other-content-id', []);
const callbackData = ethers.AbiCoder.defaultAbiCoder().encode([], []);
await dao.initiateValidationPool(
'some-content-id',
@ -46,7 +46,7 @@ describe('Proposal', () => {
await dao.evaluateOutcome(1);
return {
dao, forum, proposals, account1, account2,
dao, proposals, account1, account2,
};
}
@ -65,7 +65,6 @@ describe('Proposal', () => {
describe('Attestation', () => {
let dao;
let forum;
let proposals;
let account1;
let account2;
@ -74,14 +73,13 @@ describe('Proposal', () => {
beforeEach(async () => {
({
dao,
forum,
proposals,
account1,
account2,
} = await loadFixture(deploy));
const emptyCallbackData = ethers.AbiCoder.defaultAbiCoder().encode([], []);
await forum.addPost([{ authorAddress: account1, weightPPM: 1000000 }], 'proposal-content-id', []);
await dao.addPost([{ authorAddress: account1, weightPPM: 1000000 }], 'proposal-content-id', []);
await proposals.propose('proposal-content-id', [20, 20, 20], false, emptyCallbackData, { value: 100 });
expect(await proposals.proposalCount()).to.equal(1);
proposal = await proposals.proposals(0);
@ -90,10 +88,10 @@ describe('Proposal', () => {
});
it('Can submit a proposal', async () => {
const post = await forum.getPost('proposal-content-id');
expect(post.authors).to.have.length(1);
expect(post.authors[0].weightPPM).to.equal(1000000);
expect(post.authors[0].authorAddress).to.equal(account1);
const postAuthors = await dao.getPostAuthors('proposal-content-id');
expect(postAuthors).to.have.length(1);
expect(postAuthors[0].weightPPM).to.equal(1000000);
expect(postAuthors[0].authorAddress).to.equal(account1);
});
it('Can attest for a proposal', async () => {

View File

@ -9,13 +9,12 @@ const deployDAO = require('./util/deploy-dao');
describe('Validation Pools', () => {
async function deploy() {
const [account1, account2] = await ethers.getSigners();
const { dao, forum } = await deployDAO();
const { dao } = await deployDAO();
return {
dao, forum, account1, account2,
dao, account1, account2,
};
}
let dao;
let forum;
let account1;
let account2;
const POOL_DURATION = 3600; // 1 hour
@ -41,9 +40,9 @@ describe('Validation Pools', () => {
beforeEach(async () => {
({
dao, forum, account1, account2,
dao, account1, account2,
} = await loadFixture(deploy));
await forum.addPost([{ weightPPM: 1000000, authorAddress: account1 }], 'content-id', []);
await dao.addPost([{ weightPPM: 1000000, authorAddress: account1 }], 'content-id', []);
const init = () => initiateValidationPool({ fee: POOL_FEE });
await expect(init()).to.emit(dao, 'ValidationPoolInitiated').withArgs(0);
expect(await dao.validationPoolCount()).to.equal(1);
@ -207,7 +206,7 @@ describe('Validation Pools', () => {
beforeEach(async () => {
time.increase(POOL_DURATION + 1);
await dao.evaluateOutcome(0);
await forum.addPost([{ weightPPM: 1000000, authorAddress: account2 }], 'content-id-2', []);
await dao.addPost([{ weightPPM: 1000000, authorAddress: account2 }], 'content-id-2', []);
const init = () => initiateValidationPool({ postId: 'content-id-2' });
await expect(init()).to.emit(dao, 'ValidationPoolInitiated').withArgs(1);
time.increase(POOL_DURATION + 1);

View File

@ -13,13 +13,13 @@ describe('Work1', () => {
// Contracts are deployed using the first signer/account by default
const [account1, account2] = await ethers.getSigners();
const { dao, forum } = await deployDAO();
const { dao } = await deployDAO();
const Proposals = await ethers.getContractFactory('Proposals');
const proposals = await Proposals.deploy(dao.target);
const Work1 = await ethers.getContractFactory('Work1');
const work1 = await Work1.deploy(dao.target, forum.target, proposals.target, WORK1_PRICE);
const work1 = await Work1.deploy(dao.target, proposals.target, WORK1_PRICE);
await forum.addPost([{ weightPPM: 1000000, authorAddress: account1 }], 'some-content-id', []);
await dao.addPost([{ weightPPM: 1000000, authorAddress: account1 }], 'some-content-id', []);
const callbackData = ethers.AbiCoder.defaultAbiCoder().encode([], []);
await dao.initiateValidationPool(
'some-content-id',
@ -36,7 +36,7 @@ describe('Work1', () => {
await dao.evaluateOutcome(0);
return {
dao, forum, work1, proposals, account1, account2,
dao, work1, proposals, account1, account2,
};
}
@ -185,14 +185,13 @@ describe('Work1', () => {
describe('Work evidence and approval/disapproval', () => {
let dao;
let forum;
let work1;
let account1;
let account2;
beforeEach(async () => {
({
dao, forum, work1, account1, account2,
dao, work1, account1, account2,
} = await loadFixture(deploy));
await dao.stakeAvailability(work1.target, 50, STAKE_DURATION);
});
@ -224,11 +223,13 @@ describe('Work1', () => {
.to.emit(work1, 'WorkApprovalSubmitted').withArgs(0, true);
expect(await dao.balanceOf(work1.target)).to.equal(0);
expect(await dao.balanceOf(account1)).to.equal(100);
const post = await forum.getPost('evidence-content-id');
const post = await dao.posts('evidence-content-id');
expect(post.sender).to.equal(work1.target);
expect(post.authors).to.have.length(1);
expect(post.authors[0].weightPPM).to.equal(1000000);
expect(post.authors[0].authorAddress).to.equal(account1);
expect(post.id).to.equal('evidence-content-id');
const postAuthors = await dao.getPostAuthors('evidence-content-id');
expect(postAuthors).to.have.length(1);
expect(postAuthors[0].weightPPM).to.equal(1000000);
expect(postAuthors[0].authorAddress).to.equal(account1);
const pool = await dao.validationPools(1);
expect(pool.props.fee).to.equal(WORK1_PRICE);
expect(pool.sender).to.equal(work1.target);

View File

@ -2,21 +2,21 @@ const { ethers } = require('hardhat');
const deployDAO = async () => {
const Reputation = await ethers.getContractFactory('Reputation');
const Forum = await ethers.getContractFactory('Forum');
const Bench = await ethers.getContractFactory('Bench');
const DAO = await ethers.getContractFactory('DAO');
const GlobalForum = await ethers.getContractFactory('GlobalForum');
const forum = await GlobalForum.deploy();
const reputation = await Reputation.deploy();
const forum = await Forum.deploy();
const bench = await Bench.deploy();
const dao = await DAO.deploy(
reputation.target,
bench.target,
forum.target,
bench.target,
);
return {
forum,
dao,
reputation,
forum,
bench,
};
};