Compare commits
4 Commits
6b37cead66
...
da20410f87
Author | SHA1 | Date |
---|---|---|
Ladd Hoffman | da20410f87 | |
Ladd Hoffman | ed928043ed | |
Ladd Hoffman | 282d9478df | |
Ladd Hoffman | ef19b9bd66 |
|
@ -0,0 +1,105 @@
|
|||
// 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;
|
||||
}
|
||||
}
|
|
@ -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_,
|
||||
Proposals proposals_,
|
||||
uint price_
|
||||
) Work(dao_, proposals_, price_) {}
|
||||
DAO dao,
|
||||
GlobalForum forum,
|
||||
Proposals proposals,
|
||||
uint price
|
||||
) Work(dao, forum, 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);
|
||||
dao.addPost(authors, request.evidencePostId, request.references);
|
||||
forum.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);
|
||||
dao.addPost(authors, request.requestPostId, emptyReferences);
|
||||
forum.addPost(authors, request.requestPostId, emptyReferences);
|
||||
dao.initiateValidationPool{value: request.fee / 10}(
|
||||
request.requestPostId,
|
||||
POOL_DURATION,
|
||||
|
|
|
@ -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. DAO.addPost should be called before this.
|
||||
/// Submit a post as a proposal. forum.addPost should be called before this.
|
||||
function propose(
|
||||
string calldata postId,
|
||||
uint[3] calldata durations,
|
||||
|
|
|
@ -9,10 +9,11 @@ abstract contract RollableWork is Work {
|
|||
|
||||
constructor(
|
||||
DAO dao,
|
||||
GlobalForum forum,
|
||||
Proposals proposalsContract,
|
||||
Rollup rollupContract_,
|
||||
uint price
|
||||
) Work(dao, proposalsContract, price) {
|
||||
) Work(dao, forum, proposalsContract, price) {
|
||||
rollupContract = rollupContract_;
|
||||
}
|
||||
|
||||
|
@ -34,7 +35,7 @@ abstract contract RollableWork is Work {
|
|||
// Make work evidence post
|
||||
Author[] memory authors = new Author[](1);
|
||||
authors[0] = Author(1000000, stake.worker);
|
||||
dao.addPost(authors, request.evidencePostId, request.references);
|
||||
forum.addPost(authors, request.evidencePostId, request.references);
|
||||
|
||||
// send worker stakes and customer fee to rollup contract
|
||||
dao.forwardAllowance(
|
||||
|
|
|
@ -2,7 +2,6 @@
|
|||
pragma solidity ^0.8.24;
|
||||
|
||||
import "./core/DAO.sol";
|
||||
import "./core/Forum.sol";
|
||||
import "./Availability.sol";
|
||||
import "./Proposals.sol";
|
||||
import "./interfaces/IOnProposalAccepted.sol";
|
||||
|
@ -31,6 +30,7 @@ abstract contract Work is Availability, IOnProposalAccepted {
|
|||
uint proposalIndex;
|
||||
}
|
||||
|
||||
GlobalForum forum;
|
||||
Proposals proposalsContract;
|
||||
uint public price;
|
||||
mapping(uint => PriceProposal) public priceProposals;
|
||||
|
@ -48,11 +48,13 @@ 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
|
||||
|
@ -107,7 +109,7 @@ abstract contract Work is Availability, IOnProposalAccepted {
|
|||
// Make work evidence post
|
||||
Author[] memory authors = new Author[](1);
|
||||
authors[0] = Author(1000000, stake.worker);
|
||||
dao.addPost(authors, request.evidencePostId, request.references);
|
||||
forum.addPost(authors, request.evidencePostId, request.references);
|
||||
emit WorkApprovalSubmitted(requestIndex, approval);
|
||||
// Initiate validation pool
|
||||
uint poolIndex = dao.initiateValidationPool{value: request.fee}(
|
||||
|
|
|
@ -7,8 +7,9 @@ import "./Proposals.sol";
|
|||
|
||||
contract Work1 is Work {
|
||||
constructor(
|
||||
DAO dao_,
|
||||
Proposals proposals_,
|
||||
uint price_
|
||||
) Work(dao_, proposals_, price_) {}
|
||||
DAO dao,
|
||||
GlobalForum forum,
|
||||
Proposals proposals,
|
||||
uint price
|
||||
) Work(dao, forum, proposals, price) {}
|
||||
}
|
||||
|
|
|
@ -9,8 +9,9 @@ import "./Rollup.sol";
|
|||
contract Work2 is RollableWork {
|
||||
constructor(
|
||||
DAO dao,
|
||||
GlobalForum forum,
|
||||
Proposals proposals,
|
||||
Rollup rollup,
|
||||
uint price
|
||||
) RollableWork(dao, proposals, rollup, price) {}
|
||||
) RollableWork(dao, forum, proposals, rollup, price) {}
|
||||
}
|
||||
|
|
|
@ -2,14 +2,7 @@
|
|||
pragma solidity ^0.8.24;
|
||||
|
||||
import "./DAO.sol";
|
||||
import "./Forum.sol";
|
||||
|
||||
struct ValidationPoolStake {
|
||||
uint id;
|
||||
bool inFavor;
|
||||
uint amount;
|
||||
address sender;
|
||||
}
|
||||
import "../GlobalForum.sol";
|
||||
|
||||
struct ValidationPoolParams {
|
||||
uint duration;
|
||||
|
@ -28,32 +21,48 @@ 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 {
|
||||
mapping(uint => ValidationPool) public validationPools;
|
||||
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;
|
||||
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];
|
||||
|
||||
function registerDAO(DAO dao_) external {
|
||||
// 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 {
|
||||
require(
|
||||
address(dao) == address(0),
|
||||
"A DAO has already been registered"
|
||||
);
|
||||
dao = dao_;
|
||||
forum = forum_;
|
||||
}
|
||||
|
||||
/// Register a stake for/against a validation pool
|
||||
|
@ -67,14 +76,14 @@ contract Bench {
|
|||
msg.sender == address(dao),
|
||||
"Only DAO contract may call stakeOnValidationPool"
|
||||
);
|
||||
ValidationPool storage pool = validationPools[poolIndex];
|
||||
Pool 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++;
|
||||
ValidationPoolStake storage s = pool.stakes[stakeIndex];
|
||||
Stake storage s = pool.stakes[stakeIndex];
|
||||
s.sender = sender;
|
||||
s.inFavor = inFavor;
|
||||
s.amount = amount;
|
||||
|
@ -107,7 +116,7 @@ contract Bench {
|
|||
require(winRatio[0] <= winRatio[1], "Win ratio is greater than one");
|
||||
require(bindingPercent <= 100, "Binding percent must be <= 100");
|
||||
poolIndex = validationPoolCount++;
|
||||
ValidationPool storage pool = validationPools[poolIndex];
|
||||
Pool storage pool = validationPools[poolIndex];
|
||||
pool.id = poolIndex;
|
||||
pool.sender = sender;
|
||||
pool.props.postId = postId;
|
||||
|
@ -134,11 +143,11 @@ contract Bench {
|
|||
msg.sender == address(dao),
|
||||
"Only DAO contract may call evaluateOutcome"
|
||||
);
|
||||
ValidationPool storage pool = validationPools[poolIndex];
|
||||
Pool storage pool = validationPools[poolIndex];
|
||||
require(pool.props.resolved == false, "Pool is already resolved");
|
||||
uint stakedFor;
|
||||
uint stakedAgainst;
|
||||
ValidationPoolStake storage s;
|
||||
Stake storage s;
|
||||
for (uint i = 0; i < pool.stakeCount; i++) {
|
||||
s = pool.stakes[i];
|
||||
// Make sure the sender still has the required balance.
|
||||
|
@ -245,9 +254,11 @@ contract Bench {
|
|||
}
|
||||
|
||||
// Transfer REP to the forum instead of to the author directly
|
||||
dao.propagateReputation(
|
||||
propagateReputation(
|
||||
pool.props.postId,
|
||||
int(pool.props.minted / 2 + remainder)
|
||||
int(pool.props.minted / 2 + remainder),
|
||||
false,
|
||||
0
|
||||
);
|
||||
} else {
|
||||
// If vote does not pass, divide the losing stake among the winners
|
||||
|
@ -283,4 +294,126 @@ 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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 "./Forum.sol";
|
||||
import "../GlobalForum.sol";
|
||||
import "../interfaces/IAcceptAvailability.sol";
|
||||
import "../interfaces/IOnValidate.sol";
|
||||
|
||||
|
@ -12,27 +12,36 @@ import "hardhat/console.sol";
|
|||
|
||||
contract DAO {
|
||||
Reputation rep;
|
||||
Forum forum;
|
||||
GlobalForum 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 PostAdded(string id);
|
||||
event LWValidationPoolInitiated(uint poolIndex);
|
||||
event LWValidationPoolResolved(
|
||||
uint poolIndex,
|
||||
bool votePasses,
|
||||
bool quorumMet
|
||||
);
|
||||
|
||||
constructor(Reputation reputation_, Forum forum_, Bench bench_) {
|
||||
constructor(Reputation reputation_, Bench bench_, GlobalForum forum_) {
|
||||
rep = reputation_;
|
||||
forum = forum_;
|
||||
bench = bench_;
|
||||
forum = forum_;
|
||||
rep.registerDAO(this);
|
||||
forum.registerDAO(this);
|
||||
bench.registerDAO(this);
|
||||
bench.registerDAO(this, forum);
|
||||
}
|
||||
|
||||
function emitPostAdded(string memory id) public {
|
||||
emit PostAdded(id);
|
||||
}
|
||||
|
||||
function emitValidationPoolInitiated(uint poolIndex) public {
|
||||
|
@ -47,13 +56,13 @@ contract DAO {
|
|||
emit ValidationPoolResolved(poolIndex, votePasses, quorumMet);
|
||||
}
|
||||
|
||||
function emitPostAdded(string memory id) public {
|
||||
emit PostAdded(id);
|
||||
function emitLWValidationPoolInitiated(uint poolIndex) public {
|
||||
emit LWValidationPoolInitiated(poolIndex);
|
||||
}
|
||||
|
||||
function update(address from, address to, uint256 value) public {
|
||||
require(
|
||||
msg.sender == address(forum) || msg.sender == address(bench),
|
||||
msg.sender == address(bench),
|
||||
"Only DAO core contracts may call update"
|
||||
);
|
||||
rep.update(from, to, value);
|
||||
|
@ -61,7 +70,7 @@ contract DAO {
|
|||
|
||||
function mint(address account, uint256 value) public {
|
||||
require(
|
||||
msg.sender == address(forum) || msg.sender == address(bench),
|
||||
msg.sender == address(bench),
|
||||
"Only DAO core contracts may call mint"
|
||||
);
|
||||
rep.mint(account, value);
|
||||
|
@ -69,7 +78,7 @@ contract DAO {
|
|||
|
||||
function burn(address account, uint256 value) public {
|
||||
require(
|
||||
msg.sender == address(forum) || msg.sender == address(bench),
|
||||
msg.sender == address(bench),
|
||||
"Only DAO core contracts may call burn"
|
||||
);
|
||||
rep.burn(account, value);
|
||||
|
@ -77,7 +86,7 @@ contract DAO {
|
|||
|
||||
function registerMember(address account) public {
|
||||
require(
|
||||
msg.sender == address(forum) || msg.sender == address(bench),
|
||||
msg.sender == address(bench),
|
||||
"Only DAO core contracts may call registerMember"
|
||||
);
|
||||
if (!isMember[account]) {
|
||||
|
@ -129,10 +138,6 @@ 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++) {
|
||||
|
@ -236,7 +241,7 @@ contract DAO {
|
|||
bytes calldata callbackData
|
||||
) public {
|
||||
require(
|
||||
msg.sender == address(forum) || msg.sender == address(bench),
|
||||
msg.sender == address(bench),
|
||||
"Only DAO core contracts may call onValidate"
|
||||
);
|
||||
IOnValidate(target).onValidate(
|
||||
|
@ -247,34 +252,6 @@ 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
|
||||
|
|
|
@ -1,251 +0,0 @@
|
|||
// 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,370 @@
|
|||
// 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
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,7 +1,7 @@
|
|||
const deployCoreContracts = require('./util/deploy-core-contracts');
|
||||
const deployDAOCoreContracts = require('./util/deploy-core-contracts');
|
||||
|
||||
async function main() {
|
||||
await deployCoreContracts();
|
||||
await deployDAOCoreContracts();
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
|
|
|
@ -1,13 +1,15 @@
|
|||
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 deployCoreContracts = require('./util/deploy-core-contracts');
|
||||
const deployDAOCoreContracts = require('./util/deploy-core-contracts');
|
||||
|
||||
const { ROLLUP_INTERVAL } = process.env;
|
||||
|
||||
async function main() {
|
||||
await deployCoreContracts();
|
||||
await deployContract('GlobalForum');
|
||||
await deployDAOCoreContracts();
|
||||
await deployDAOContract('Rollup', [ROLLUP_INTERVAL]);
|
||||
await deployDAOContract('Proposals');
|
||||
await deployWorkContract('Work1');
|
||||
|
|
|
@ -4,15 +4,14 @@ const contractAddresses = require('../../contract-addresses.json');
|
|||
|
||||
const network = process.env.HARDHAT_NETWORK;
|
||||
|
||||
const deployCoreContracts = async () => {
|
||||
const deployDAOCoreContracts = async () => {
|
||||
await deployContract('Reputation', [], true);
|
||||
await deployContract('Forum', [], true);
|
||||
await deployContract('Bench', [], true);
|
||||
await deployContract('DAO', [
|
||||
contractAddresses[network].Reputation,
|
||||
contractAddresses[network].Forum,
|
||||
contractAddresses[network].GlobalForum,
|
||||
contractAddresses[network].Bench,
|
||||
], true);
|
||||
};
|
||||
|
||||
module.exports = deployCoreContracts;
|
||||
module.exports = deployDAOCoreContracts;
|
||||
|
|
|
@ -13,6 +13,7 @@ const deployWorkContract = async (name) => {
|
|||
|
||||
await deployContract(name, [
|
||||
contractAddresses[network].DAO,
|
||||
contractAddresses[network].GlobalForum,
|
||||
contractAddresses[network].Proposals,
|
||||
price]);
|
||||
};
|
||||
|
|
|
@ -8,16 +8,18 @@ const deployDAO = require('./util/deploy-dao');
|
|||
|
||||
describe('Forum', () => {
|
||||
async function deploy() {
|
||||
const [account1, account2, account3] = await ethers.getSigners();
|
||||
const { dao } = await deployDAO();
|
||||
const [account1, account2, account3, account4] = await ethers.getSigners();
|
||||
const { dao, forum } = await deployDAO();
|
||||
return {
|
||||
dao, account1, account2, account3,
|
||||
dao, forum, account1, account2, account3, account4,
|
||||
};
|
||||
}
|
||||
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([], []);
|
||||
|
@ -39,7 +41,7 @@ describe('Forum', () => {
|
|||
{ value: fee ?? POOL_FEE },
|
||||
);
|
||||
|
||||
const addPost = (author, postId, references) => dao.addPost([{
|
||||
const addPost = (author, postId, references) => forum.addPost([{
|
||||
weightPPM: 1000000,
|
||||
authorAddress: author,
|
||||
}], postId, references);
|
||||
|
@ -47,49 +49,43 @@ describe('Forum', () => {
|
|||
describe('Post', () => {
|
||||
beforeEach(async () => {
|
||||
({
|
||||
dao, account1, account2, account3,
|
||||
dao, forum, account1, account2, account3, account4,
|
||||
} = await loadFixture(deploy));
|
||||
});
|
||||
|
||||
it('should be able to add a post', async () => {
|
||||
const postId = 'some-id';
|
||||
await expect(addPost(account1, postId, [])).to.emit(dao, 'PostAdded').withArgs('some-id');
|
||||
const post = await dao.posts(postId);
|
||||
await expect(addPost(account1, postId, [])).to.emit(forum, 'PostAdded').withArgs('some-id');
|
||||
const post = await forum.getPost(postId);
|
||||
expect(post.sender).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);
|
||||
expect(post.authors).to.have.length(1);
|
||||
expect(post.authors[0].weightPPM).to.equal(1000000);
|
||||
expect(post.authors[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 dao.posts(postId);
|
||||
const post = await forum.getPost(postId);
|
||||
expect(post.sender).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(account2);
|
||||
expect(post.authors).to.have.length(1);
|
||||
expect(post.authors[0].weightPPM).to.equal(1000000);
|
||||
expect(post.authors[0].authorAddress).to.equal(account2);
|
||||
});
|
||||
|
||||
it('should be able to add a post with multiple authors', async () => {
|
||||
const postId = 'some-id';
|
||||
await expect(dao.addPost([
|
||||
await expect(forum.addPost([
|
||||
{ weightPPM: 500000, authorAddress: account1 },
|
||||
{ weightPPM: 500000, authorAddress: account2 },
|
||||
], postId, [])).to.emit(dao, 'PostAdded').withArgs('some-id');
|
||||
const post = await dao.posts(postId);
|
||||
], postId, [])).to.emit(forum, 'PostAdded').withArgs('some-id');
|
||||
const post = await forum.getPost(postId);
|
||||
expect(post.sender).to.equal(account1);
|
||||
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);
|
||||
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);
|
||||
await initiateValidationPool({ postId: 'some-id' });
|
||||
await time.increase(POOL_DURATION + 1);
|
||||
await dao.evaluateOutcome(0);
|
||||
|
@ -99,7 +95,7 @@ describe('Forum', () => {
|
|||
|
||||
it('should not be able to add a post with total author weight < 100%', async () => {
|
||||
const postId = 'some-id';
|
||||
await expect(dao.addPost([
|
||||
await expect(forum.addPost([
|
||||
{ weightPPM: 500000, authorAddress: account1 },
|
||||
{ weightPPM: 400000, authorAddress: account2 },
|
||||
], postId, [])).to.be.rejectedWith('Author weights must sum to 1000000');
|
||||
|
@ -107,7 +103,7 @@ describe('Forum', () => {
|
|||
|
||||
it('should not be able to add a post with total author weight > 100%', async () => {
|
||||
const postId = 'some-id';
|
||||
await expect(dao.addPost([
|
||||
await expect(forum.addPost([
|
||||
{ weightPPM: 500000, authorAddress: account1 },
|
||||
{ weightPPM: 600000, authorAddress: account2 },
|
||||
], postId, [])).to.be.rejectedWith('Author weights must sum to 1000000');
|
||||
|
@ -126,13 +122,10 @@ 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');
|
||||
|
@ -140,8 +133,6 @@ 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 () => {
|
||||
|
@ -221,7 +212,6 @@ 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);
|
||||
|
@ -263,21 +253,15 @@ describe('Forum', () => {
|
|||
|
||||
it('should enforce depth limit', async () => {
|
||||
await addPost(account1, 'content-id-1', []);
|
||||
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 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 initiateValidationPool({ postId: 'content-id-4' });
|
||||
await dao.evaluateOutcome(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);
|
||||
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);
|
||||
});
|
||||
|
||||
it('should be able to incinerate reputation', async () => {
|
||||
|
@ -290,7 +274,6 @@ 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);
|
||||
});
|
||||
|
||||
|
@ -301,7 +284,6 @@ 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' });
|
||||
|
@ -310,8 +292,6 @@ 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' });
|
||||
|
@ -322,8 +302,6 @@ 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 () => {
|
||||
|
@ -336,9 +314,6 @@ 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 () => {
|
||||
|
@ -351,9 +326,6 @@ 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 () => {
|
||||
|
@ -366,15 +338,12 @@ 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 dao.addPost([
|
||||
await forum.addPost([
|
||||
{ weightPPM: 500000, authorAddress: account1 },
|
||||
{ weightPPM: 500000, authorAddress: account2 },
|
||||
], 'content-id', []);
|
||||
|
@ -383,7 +352,6 @@ 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' });
|
||||
|
@ -394,7 +362,6 @@ 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 () => {
|
||||
|
@ -404,11 +371,9 @@ 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(125);
|
||||
expect(await dao.balanceOf(account3)).to.equal(475);
|
||||
expect(await dao.balanceOf(account2)).to.equal(0);
|
||||
expect(await dao.balanceOf(account3)).to.equal(600);
|
||||
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 () => {
|
||||
|
@ -421,8 +386,6 @@ 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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -13,13 +13,13 @@ describe('Onboarding', () => {
|
|||
// Contracts are deployed using the first signer/account by default
|
||||
const [account1, account2] = await ethers.getSigners();
|
||||
|
||||
const { dao } = await deployDAO();
|
||||
const { dao, forum } = 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, proposals.target, PRICE);
|
||||
const onboarding = await Onboarding.deploy(dao.target, forum.target, proposals.target, PRICE);
|
||||
|
||||
await dao.addPost([{ weightPPM: 1000000, authorAddress: account1 }], 'content-id', []);
|
||||
await forum.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, onboarding, account1, account2,
|
||||
dao, forum, onboarding, account1, account2,
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -53,13 +53,14 @@ describe('Onboarding', () => {
|
|||
|
||||
describe('Work approval/disapproval', () => {
|
||||
let dao;
|
||||
let forum;
|
||||
let onboarding;
|
||||
let account1;
|
||||
let account2;
|
||||
|
||||
beforeEach(async () => {
|
||||
({
|
||||
dao, onboarding, account1, account2,
|
||||
dao, forum, onboarding, account1, account2,
|
||||
} = await loadFixture(deploy));
|
||||
await dao.stakeAvailability(onboarding.target, 50, STAKE_DURATION);
|
||||
});
|
||||
|
@ -70,13 +71,11 @@ describe('Onboarding', () => {
|
|||
await expect(onboarding.submitWorkApproval(0, true))
|
||||
.to.emit(dao, 'ValidationPoolInitiated').withArgs(1)
|
||||
.to.emit(onboarding, 'WorkApprovalSubmitted').withArgs(0, true);
|
||||
const post = await dao.posts('evidence-content-id');
|
||||
const post = await forum.getPost('evidence-content-id');
|
||||
expect(post.sender).to.equal(onboarding.target);
|
||||
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);
|
||||
expect(post.authors).to.have.length(1);
|
||||
expect(post.authors[0].weightPPM).to.equal(1000000);
|
||||
expect(post.authors[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);
|
||||
|
@ -114,7 +113,7 @@ describe('Onboarding', () => {
|
|||
describe('Onboarding followup', () => {
|
||||
it('resolving the first validation pool should trigger a second pool', async () => {
|
||||
const {
|
||||
dao, onboarding, account2,
|
||||
dao, forum, onboarding, account2,
|
||||
} = await loadFixture(deploy);
|
||||
await dao.stakeAvailability(onboarding.target, 50, STAKE_DURATION);
|
||||
await onboarding.connect(account2).requestWork('req-content-id', { value: PRICE });
|
||||
|
@ -122,14 +121,12 @@ 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 dao.postCount()).to.equal(3);
|
||||
const post = await dao.posts('req-content-id');
|
||||
expect(await forum.postCount()).to.equal(3);
|
||||
const post = await forum.getPost('req-content-id');
|
||||
expect(post.sender).to.equal(onboarding.target);
|
||||
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);
|
||||
expect(post.authors).to.have.length(1);
|
||||
expect(post.authors[0].weightPPM).to.equal(1000000);
|
||||
expect(post.authors[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);
|
||||
|
@ -139,7 +136,7 @@ describe('Onboarding', () => {
|
|||
|
||||
it('if the first validation pool is rejected it should not trigger a second pool', async () => {
|
||||
const {
|
||||
dao, onboarding, account2,
|
||||
dao, forum, onboarding, account2,
|
||||
} = await loadFixture(deploy);
|
||||
await dao.stakeAvailability(onboarding.target, 40, STAKE_DURATION);
|
||||
await onboarding.connect(account2).requestWork('req-content-id', { value: PRICE });
|
||||
|
@ -148,7 +145,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 dao.postCount()).to.equal(2);
|
||||
expect(await forum.postCount()).to.equal(2);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -12,12 +12,12 @@ describe('Proposal', () => {
|
|||
// Contracts are deployed using the first signer/account by default
|
||||
const [account1, account2] = await ethers.getSigners();
|
||||
|
||||
const { dao } = await deployDAO();
|
||||
const { dao, forum } = await deployDAO();
|
||||
const Proposals = await ethers.getContractFactory('Proposals');
|
||||
const proposals = await Proposals.deploy(dao.target);
|
||||
|
||||
await dao.addPost([{ weightPPM: 1000000, authorAddress: account1 }], 'some-content-id', []);
|
||||
await dao.addPost([{ weightPPM: 1000000, authorAddress: account2 }], 'some-other-content-id', []);
|
||||
await forum.addPost([{ weightPPM: 1000000, authorAddress: account1 }], 'some-content-id', []);
|
||||
await forum.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, proposals, account1, account2,
|
||||
dao, forum, proposals, account1, account2,
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -65,6 +65,7 @@ describe('Proposal', () => {
|
|||
|
||||
describe('Attestation', () => {
|
||||
let dao;
|
||||
let forum;
|
||||
let proposals;
|
||||
let account1;
|
||||
let account2;
|
||||
|
@ -73,13 +74,14 @@ describe('Proposal', () => {
|
|||
beforeEach(async () => {
|
||||
({
|
||||
dao,
|
||||
forum,
|
||||
proposals,
|
||||
account1,
|
||||
account2,
|
||||
} = await loadFixture(deploy));
|
||||
|
||||
const emptyCallbackData = ethers.AbiCoder.defaultAbiCoder().encode([], []);
|
||||
await dao.addPost([{ authorAddress: account1, weightPPM: 1000000 }], 'proposal-content-id', []);
|
||||
await forum.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);
|
||||
|
@ -88,10 +90,10 @@ describe('Proposal', () => {
|
|||
});
|
||||
|
||||
it('Can submit a proposal', async () => {
|
||||
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);
|
||||
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);
|
||||
});
|
||||
|
||||
it('Can attest for a proposal', async () => {
|
||||
|
|
|
@ -9,12 +9,13 @@ const deployDAO = require('./util/deploy-dao');
|
|||
describe('Validation Pools', () => {
|
||||
async function deploy() {
|
||||
const [account1, account2] = await ethers.getSigners();
|
||||
const { dao } = await deployDAO();
|
||||
const { dao, forum } = await deployDAO();
|
||||
return {
|
||||
dao, account1, account2,
|
||||
dao, forum, account1, account2,
|
||||
};
|
||||
}
|
||||
let dao;
|
||||
let forum;
|
||||
let account1;
|
||||
let account2;
|
||||
const POOL_DURATION = 3600; // 1 hour
|
||||
|
@ -40,9 +41,9 @@ describe('Validation Pools', () => {
|
|||
|
||||
beforeEach(async () => {
|
||||
({
|
||||
dao, account1, account2,
|
||||
dao, forum, account1, account2,
|
||||
} = await loadFixture(deploy));
|
||||
await dao.addPost([{ weightPPM: 1000000, authorAddress: account1 }], 'content-id', []);
|
||||
await forum.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);
|
||||
|
@ -206,7 +207,7 @@ describe('Validation Pools', () => {
|
|||
beforeEach(async () => {
|
||||
time.increase(POOL_DURATION + 1);
|
||||
await dao.evaluateOutcome(0);
|
||||
await dao.addPost([{ weightPPM: 1000000, authorAddress: account2 }], 'content-id-2', []);
|
||||
await forum.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);
|
||||
|
|
|
@ -13,13 +13,13 @@ describe('Work1', () => {
|
|||
// Contracts are deployed using the first signer/account by default
|
||||
const [account1, account2] = await ethers.getSigners();
|
||||
|
||||
const { dao } = await deployDAO();
|
||||
const { dao, forum } = 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, proposals.target, WORK1_PRICE);
|
||||
const work1 = await Work1.deploy(dao.target, forum.target, proposals.target, WORK1_PRICE);
|
||||
|
||||
await dao.addPost([{ weightPPM: 1000000, authorAddress: account1 }], 'some-content-id', []);
|
||||
await forum.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, work1, proposals, account1, account2,
|
||||
dao, forum, work1, proposals, account1, account2,
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -185,13 +185,14 @@ describe('Work1', () => {
|
|||
|
||||
describe('Work evidence and approval/disapproval', () => {
|
||||
let dao;
|
||||
let forum;
|
||||
let work1;
|
||||
let account1;
|
||||
let account2;
|
||||
|
||||
beforeEach(async () => {
|
||||
({
|
||||
dao, work1, account1, account2,
|
||||
dao, forum, work1, account1, account2,
|
||||
} = await loadFixture(deploy));
|
||||
await dao.stakeAvailability(work1.target, 50, STAKE_DURATION);
|
||||
});
|
||||
|
@ -223,13 +224,11 @@ 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 dao.posts('evidence-content-id');
|
||||
const post = await forum.getPost('evidence-content-id');
|
||||
expect(post.sender).to.equal(work1.target);
|
||||
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);
|
||||
expect(post.authors).to.have.length(1);
|
||||
expect(post.authors[0].weightPPM).to.equal(1000000);
|
||||
expect(post.authors[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);
|
||||
|
|
|
@ -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,
|
||||
forum.target,
|
||||
bench.target,
|
||||
forum.target,
|
||||
);
|
||||
return {
|
||||
forum,
|
||||
dao,
|
||||
reputation,
|
||||
forum,
|
||||
bench,
|
||||
};
|
||||
};
|
||||
|
|
Loading…
Reference in New Issue