update DAO to use global forum

This commit is contained in:
Ladd Hoffman 2024-06-28 13:44:18 -05:00
parent ed928043ed
commit da20410f87
20 changed files with 297 additions and 454 deletions

View File

@ -20,17 +20,16 @@ struct Post {
}
contract GlobalForum {
mapping(string => Post) public posts;
mapping(string => Post) posts;
string[] public postIds;
uint public postCount;
event PostAdded(string id);
function addPost(
string calldata postId,
Author[] calldata authors,
Reference[] calldata references,
string calldata content
string calldata postId,
Reference[] calldata references
) external {
require(authors.length > 0, "Post must include at least one author");
postCount++;
@ -42,7 +41,6 @@ contract GlobalForum {
);
post.sender = msg.sender;
post.id = postId;
post.content = content;
uint authorTotalWeightPPM;
for (uint i = 0; i < authors.length; i++) {
authorTotalWeightPPM += authors[i].weightPPM;
@ -87,4 +85,21 @@ contract GlobalForum {
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_,
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,

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. 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,

View File

@ -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(

View File

@ -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}(

View File

@ -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) {}
}

View File

@ -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) {}
}

View File

@ -2,7 +2,7 @@
pragma solidity ^0.8.24;
import "./DAO.sol";
import "./Forum.sol";
import "../GlobalForum.sol";
struct ValidationPoolParams {
uint duration;
@ -43,17 +43,26 @@ contract Bench {
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
@ -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);
}
}

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 "./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

View File

@ -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;
}
}
}
}

View File

@ -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) => {

View File

@ -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');

View File

@ -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;

View File

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

View File

@ -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);
});
});
});

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 } = 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);
});
});
});

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 } = 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 () => {

View File

@ -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);

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 } = 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);

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,
forum.target,
bench.target,
forum.target,
);
return {
forum,
dao,
reputation,
forum,
bench,
};
};