deploy core contracts separately due to size limit
Gitea Actions Demo / Explore-Gitea-Actions (push) Failing after 34s Details

This commit is contained in:
Ladd Hoffman 2024-04-30 15:56:43 -05:00
parent 5aee8c9314
commit 40c1fd43d7
50 changed files with 3072 additions and 929 deletions

View File

@ -1,11 +1,14 @@
{
"localhost": {
"DAO": "0x87933c1e51FdF52C58ee54348a9372bbDeE9A8Dc",
"Work1": "0x215078c5cf21ffB79Ee14Cf09156B94a11b7340f",
"Onboarding": "0x3c2820D27e7470075d16856D7D555FD5011451Ec",
"Proposals": "0xCd5881DB1aa6b86283a9c5660FaB65C989cf2721",
"Rollup": "0x8611676563EBcd69dC52E5829bF2914A957398C3",
"Work2": "0xC6BF1b68311e891D2BF41E4A3CB350a403831Ccd"
"DAO": "0x3287061aDCeE36C1aae420a06E4a5EaE865Fe3ce",
"Work1": "0x76Dfe9F47f06112a1b78960bf37d87CfbB6D6133",
"Onboarding": "0xd2845aE812Ee42cF024fB4C55c052365792aBd78",
"Proposals": "0x8688E736D0D72161db4D25f68EF7d0EE4856ba19",
"Rollup": "0x8BDA04936887cF11263B87185E4D19e8158c6296",
"Work2": "0xf15aCe29E5e3e4bb31FCddF2C65DF7C440449a57",
"Reputation": "0xC0f00E5915F9abE6476858fD1961EAf79395ea64",
"Forum": "0x3734B0944ea37694E85AEF60D5b256d19EDA04be",
"Bench": "0x71cb20D63576a0Fa4F620a2E96C73F82848B09e1"
},
"sepolia": {
"DAO": "0x8e5bd58B2ca8910C5F9be8de847d6883B15c60d2",

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -90,9 +90,9 @@ const generatePost = async (paper) => {
throw new Error('Paper has no authors with id');
}
const firstAuthorWallet = new ethers.Wallet(authorsInfo[0].authorPrivKey);
const eachAuthorWeightPercent = Math.floor(1000000 / authorsInfo.length);
const eachAuthorWeightPPM = Math.floor(1000000 / authorsInfo.length);
const authors = authorsInfo.map(({ authorAddress }) => ({
weightPPM: eachAuthorWeightPercent,
weightPPM: eachAuthorWeightPPM,
authorAddress,
}));
// Make sure author weights sum to 100%
@ -136,7 +136,7 @@ const importPaper = async (paper) => {
console.log('references count:', paper.references.length);
const { paperId } = paper;
const references = paper.references.filter((x) => !!x.paperId);
const eachCitationWeightPercent = Math.floor(PPM_TO_CITATIONS / references.length);
const eachCitationWeightPPM = Math.floor(PPM_TO_CITATIONS / references.length);
const citations = (await Promise.mapSeries(
references,
async (citedPaper) => {
@ -147,7 +147,7 @@ const importPaper = async (paper) => {
const citedPost = await generatePost(citedPaper);
const citedPostHash = objectHash(citedPost);
return {
weightPPM: eachCitationWeightPercent,
weightPPM: eachCitationWeightPPM,
targetPostId: citedPostHash,
};
} catch (e) {

View File

@ -1,3 +0,0 @@
module.exports = {
start: () => {},
};

View File

@ -1,99 +0,0 @@
const { dao, proposals } = require('../util/contracts');
const read = require('../util/forum/read');
const {
START_PROPOSAL_ID,
STOP_PROPOSAL_ID,
ENABLE_STAKING,
} = process.env;
let enableStaking = true;
// Subscribe to proposal events
const start = async () => {
if (ENABLE_STAKING === 'false') {
console.log('STAKING DISABLED');
enableStaking = false;
} else {
if (STOP_PROPOSAL_ID) {
enableStaking = false;
// Check for status
const proposal = await proposals.proposals(STOP_PROPOSAL_ID);
if (proposal.stage === BigInt(5)) {
// Proposal is accepted
enableStaking = false;
console.log(`STOP_PROPOSAL_ID ${STOP_PROPOSAL_ID} proposal is accepted. Diabling staking.`);
} else if (proposal.stage === BigInt(4)) {
// Proposal is failed
console.log(`STOP_PROPOSAL_ID ${STOP_PROPOSAL_ID} proposal is failed. No effect.`);
} else {
// Register a ProposalAccepted event handler.
console.log(`STOP_PROPOSAL_ID ${STOP_PROPOSAL_ID} proposal is stage ${proposal.stage.toString()}. Registering listener.`);
const proposalAcceptedHandler = (proposalIndex) => {
if (proposalIndex === STOP_PROPOSAL_ID) {
console.log(`STOP_PROPOSAL_ID ${STOP_PROPOSAL_ID} proposal is accepted. Disabling staking.`);
enableStaking = false;
proposals.off('ProposalAccepted', proposalAcceptedHandler);
}
};
proposals.on('ProposalAccepted', proposalAcceptedHandler);
}
}
if (START_PROPOSAL_ID) {
enableStaking = false;
// Check for status
const proposal = await proposals.proposals(START_PROPOSAL_ID);
if (proposal.stage === BigInt(5)) {
// Proposal is accepted
enableStaking = true;
console.log(`START_PROPOSAL_ID ${START_PROPOSAL_ID} proposal is accepted. Enabling staking.`);
} else if (proposal.stage === BigInt(4)) {
// Proposal is failed
console.log(`START_PROPOSAL_ID ${START_PROPOSAL_ID} proposal is failed. Disabling staking.`);
} else {
// Register a ProposalAccepted event handler.
console.log(`START_PROPOSAL_ID ${START_PROPOSAL_ID} proposal is stage ${proposal.stage.toString()}. Registering listener.`);
const proposalAcceptedHandler = (proposalIndex) => {
if (proposalIndex === START_PROPOSAL_ID) {
console.log(`START_PROPOSAL_ID ${START_PROPOSAL_ID} proposal is accepted. Enabling staking.`);
enableStaking = true;
proposals.off('ProposalAccepted', proposalAcceptedHandler);
}
};
proposals.on('ProposalAccepted', proposalAcceptedHandler);
}
}
}
console.log('registering validation pool listener');
dao.on('ValidationPoolInitiated', async (poolIndex) => {
console.log('Validation Pool Initiated, index', poolIndex);
const pool = await dao.validationPools(poolIndex);
console.log('postId:', pool.postId);
// Read post from database
let post;
try {
post = await read(pool.postId);
} catch (e) {
// Post not found
console.error(`error: post for validation pool ${poolIndex} not found`);
return;
}
console.log('post.content:', post.content);
// We have the opportunity to stake for/against this validation pool.
// To implement the legislative process of upgrading this protocol,
// the execution of this code can be protected by a given proposal.
// The code will only execute if the proposal has been accepted.
if (!enableStaking) return;
console.log('considering staking');
});
};
module.exports = {
start,
};

View File

@ -2,7 +2,7 @@ require('dotenv').config();
const api = require('./api');
const matrixBot = require('./matrix-bot');
const contractListeners = require('./contract-listeners');
const topics = require('./topics');
const {
ENABLE_API,
@ -17,4 +17,4 @@ if (ENABLE_MATRIX !== 'false') {
matrixBot.start();
}
contractListeners.start();
topics.start();

View File

@ -1,12 +1,12 @@
const proposals = require('./proposals');
const proposalsNotifier = require('./proposals-notifier');
const validationPools = require('./validation-pools');
const work1 = require('./work1');
const rollup = require('./rollup');
const start = () => {
proposals.start();
proposalsNotifier.start();
validationPools.start();
work1.start();
rollup.start();
};

View File

@ -0,0 +1,24 @@
const { getContractAddressByNetworkName } = require('../util/contract-config');
const { registerDecider } = require('./validation-pools');
const {
ETH_NETWORK,
} = process.env;
const rollupAddress = getContractAddressByNetworkName(ETH_NETWORK, 'Rollup');
const start = async () => {
console.log('registering validation pool decider for rollup');
registerDecider((pool, post) => {
// If this is not sent by the work1 contract, it's not of interest here.
if (pool.sender !== rollupAddress) return false;
// TODO: to derive the expected content here, we need information from Matrix.
return false;
});
};
module.exports = {
start,
};

View File

@ -0,0 +1,60 @@
const Promise = require('bluebird');
const { dao } = require('../util/contracts');
const read = require('../util/forum/read');
const gateByProposal = require('../util/gate-by-proposal');
const {
ENABLE_STAKING,
} = process.env;
const deciders = [];
const registerDecider = (decider) => {
deciders.push(decider);
};
let enableStaking;
if (ENABLE_STAKING === 'false') {
console.log('STAKING DISABLED');
enableStaking = false;
} else {
gateByProposal(((enable) => {
enableStaking = enable;
}));
}
const start = async () => {
dao.on('ValidationPoolInitiated', async (poolIndex) => {
console.log('Validation Pool Initiated, index', poolIndex);
const pool = await dao.validationPools(poolIndex);
// Read post from database
let post;
try {
post = await read(pool.props.postId);
} catch (e) {
// Post not found
console.error(`error: post for validation pool ${poolIndex} not found`);
return;
}
console.log('postId:', pool.props.postId);
console.log('post.content:', post.content);
const results = await Promise.mapSeries(deciders, (decider) => decider(pool, post));
const inFavor = results.some((x) => x === true);
// We have the opportunity to stake for/against this validation pool.
// To implement the legislative process of upgrading this protocol,
// the execution of this code can be protected by a given proposal.
// The code will only execute if the proposal has been accepted.
if (!enableStaking) {
return;
}
console.log(`WOULD STAKE ${inFavor ? 'in favor of' : 'against'} pool ${poolIndex}`);
// TODO: Stake half of available reputation
});
};
module.exports = {
start,
registerDecider,
};

View File

@ -0,0 +1,26 @@
const { getContractAddressByNetworkName } = require('../util/contract-config');
const { registerDecider } = require('./validation-pools');
const {
ETH_NETWORK,
} = process.env;
const work1Address = getContractAddressByNetworkName(ETH_NETWORK, 'Work1');
const start = async () => {
console.log('registering validation pool decider for work1');
registerDecider((pool, post) => {
// If this is not sent by the work1 contract, it's not of interest here.
if (pool.sender !== work1Address) return false;
const expectedContent = 'This is a work evidence post';
const result = post.content.startsWith(expectedContent);
console.log(`Work evidence ${result ? 'matched' : 'did not match'} the expected content`);
return result;
});
};
module.exports = {
start,
};

View File

@ -0,0 +1,61 @@
const { proposals } = require('./contracts');
const {
START_PROPOSAL_ID,
STOP_PROPOSAL_ID,
} = process.env;
const gateByProposal = async (enable) => {
enable(true);
if (STOP_PROPOSAL_ID) {
enable(false);
// Check for status
const proposal = await proposals.proposals(STOP_PROPOSAL_ID);
if (proposal.stage === BigInt(5)) {
// Proposal is accepted
enable(false);
console.log(`STOP_PROPOSAL_ID ${STOP_PROPOSAL_ID} proposal is accepted. Diabling staking.`);
} else if (proposal.stage === BigInt(4)) {
// Proposal is failed
console.log(`STOP_PROPOSAL_ID ${STOP_PROPOSAL_ID} proposal is failed. No effect.`);
} else {
// Register a ProposalAccepted event handler.
console.log(`STOP_PROPOSAL_ID ${STOP_PROPOSAL_ID} proposal is stage ${proposal.stage.toString()}. Registering listener.`);
const proposalAcceptedHandler = (proposalIndex) => {
if (proposalIndex === STOP_PROPOSAL_ID) {
console.log(`STOP_PROPOSAL_ID ${STOP_PROPOSAL_ID} proposal is accepted. Disabling staking.`);
enable(false);
proposals.off('ProposalAccepted', proposalAcceptedHandler);
}
};
proposals.on('ProposalAccepted', proposalAcceptedHandler);
}
}
if (START_PROPOSAL_ID) {
enable(false);
// Check for status
const proposal = await proposals.proposals(START_PROPOSAL_ID);
if (proposal.stage === BigInt(5)) {
// Proposal is accepted
enable(true);
console.log(`START_PROPOSAL_ID ${START_PROPOSAL_ID} proposal is accepted. Enabling staking.`);
} else if (proposal.stage === BigInt(4)) {
// Proposal is failed
console.log(`START_PROPOSAL_ID ${START_PROPOSAL_ID} proposal is failed. Disabling staking.`);
} else {
// Register a ProposalAccepted event handler.
console.log(`START_PROPOSAL_ID ${START_PROPOSAL_ID} proposal is stage ${proposal.stage.toString()}. Registering listener.`);
const proposalAcceptedHandler = (proposalIndex) => {
if (proposalIndex === START_PROPOSAL_ID) {
console.log(`START_PROPOSAL_ID ${START_PROPOSAL_ID} proposal is accepted. Enabling staking.`);
enable(true);
proposals.off('ProposalAccepted', proposalAcceptedHandler);
}
};
proposals.on('ProposalAccepted', proposalAcceptedHandler);
}
}
};
export default gateByProposal;

View File

@ -1,11 +1,14 @@
{
"localhost": {
"DAO": "0x87933c1e51FdF52C58ee54348a9372bbDeE9A8Dc",
"Work1": "0x215078c5cf21ffB79Ee14Cf09156B94a11b7340f",
"Onboarding": "0x3c2820D27e7470075d16856D7D555FD5011451Ec",
"Proposals": "0xCd5881DB1aa6b86283a9c5660FaB65C989cf2721",
"Rollup": "0x8611676563EBcd69dC52E5829bF2914A957398C3",
"Work2": "0xC6BF1b68311e891D2BF41E4A3CB350a403831Ccd"
"DAO": "0x3287061aDCeE36C1aae420a06E4a5EaE865Fe3ce",
"Work1": "0x76Dfe9F47f06112a1b78960bf37d87CfbB6D6133",
"Onboarding": "0xd2845aE812Ee42cF024fB4C55c052365792aBd78",
"Proposals": "0x8688E736D0D72161db4D25f68EF7d0EE4856ba19",
"Rollup": "0x8BDA04936887cF11263B87185E4D19e8158c6296",
"Work2": "0xf15aCe29E5e3e4bb31FCddF2C65DF7C440449a57",
"Reputation": "0xC0f00E5915F9abE6476858fD1961EAf79395ea64",
"Forum": "0x3734B0944ea37694E85AEF60D5b256d19EDA04be",
"Bench": "0x71cb20D63576a0Fa4F620a2E96C73F82848B09e1"
},
"sepolia": {
"DAO": "0x8e5bd58B2ca8910C5F9be8de847d6883B15c60d2",

View File

@ -60,7 +60,7 @@ contract Onboarding is Work, IOnValidate {
uint,
uint,
bytes calldata callbackData
) external returns (uint) {
) external {
require(
msg.sender == address(dao),
"onValidate may only be called by the DAO contract"
@ -70,7 +70,7 @@ contract Onboarding is Work, IOnValidate {
if (!votePasses || !quorumMet) {
// refund the customer the remaining amount
payable(request.customer).transfer(request.fee / 10);
return 1;
return;
}
// Make onboarding post
Citation[] memory emptyCitations;
@ -87,6 +87,5 @@ contract Onboarding is Work, IOnValidate {
false,
""
);
return 0;
}
}

View File

@ -153,7 +153,7 @@ contract Proposals is DAOContract, IOnValidate {
uint stakedFor,
uint stakedAgainst,
bytes calldata callbackData
) external returns (uint) {
) external {
require(
msg.sender == address(dao),
"onValidate may only be called by the DAO contract"
@ -177,7 +177,7 @@ contract Proposals is DAOContract, IOnValidate {
proposal.stage = Stage.Failed;
emit ProposalFailed(proposalIndex, "Quorum not met");
proposal.remainingFee += fee;
return 1;
return;
}
// Participation threshold of 50%
@ -238,7 +238,7 @@ contract Proposals is DAOContract, IOnValidate {
} else if (proposal.stage == Stage.Referendum100) {
initiateValidationPool(proposalIndex, 2, proposal.fee / 10);
}
return 0;
return;
}
/// External function that will advance a proposal to the referendum process

View File

@ -1,9 +1,8 @@
// SPDX-License-Identifier: Unlicense
pragma solidity ^0.8.24;
import "./Reputation.sol";
import "./DAO.sol";
import "./Forum.sol";
import "../interfaces/IOnValidate.sol";
struct ValidationPoolStake {
uint id;
@ -20,45 +19,55 @@ struct ValidationPoolParams {
bool redistributeLosingStakes;
}
struct ValidationPool {
uint id;
struct ValidationPoolProps {
string postId;
address sender;
uint minted;
mapping(uint => ValidationPoolStake) stakes;
uint stakeCount;
ValidationPoolParams params;
uint fee;
uint minted;
uint endTime;
bool resolved;
bool outcome;
}
struct ValidationPool {
uint id;
address sender;
mapping(uint => ValidationPoolStake) stakes;
uint stakeCount;
ValidationPoolParams params;
ValidationPoolProps props;
bool callbackOnValidate;
bytes callbackData;
}
contract ValidationPools is Reputation, Forum {
contract Bench {
mapping(uint => ValidationPool) 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];
event ValidationPoolInitiated(uint poolIndex);
event ValidationPoolResolved(
uint poolIndex,
bool votePasses,
bool quorumMet
);
function registerDAO(DAO dao_) external {
require(
address(dao) == address(0),
"A DAO has already been registered"
);
dao = dao_;
}
/// Internal function to register a stake for/against a validation pool
function _stakeOnValidationPool(
ValidationPool storage pool,
/// Register a stake for/against a validation pool
function stakeOnValidationPool(
uint poolIndex,
address sender,
uint256 amount,
bool inFavor
) internal {
require(block.timestamp <= pool.endTime, "Pool end time has passed");
) external {
ValidationPool storage pool = validationPools[poolIndex];
require(
block.timestamp <= pool.props.endTime,
"Pool end time has passed"
);
// We don't call _update here; We defer that until evaluateOutcome.
uint stakeIndex = pool.stakeCount++;
ValidationPoolStake storage s = pool.stakes[stakeIndex];
@ -68,33 +77,9 @@ contract ValidationPools is Reputation, Forum {
s.id = stakeIndex;
}
/// Accept reputation stakes toward a validation pool
function stakeOnValidationPool(
uint poolIndex,
uint256 amount,
bool inFavor
) public {
ValidationPool storage pool = validationPools[poolIndex];
_stakeOnValidationPool(pool, msg.sender, amount, inFavor);
}
/// Accept reputation stakes toward a validation pool
function delegatedStakeOnValidationPool(
uint poolIndex,
address owner,
uint256 amount,
bool inFavor
) public {
ValidationPool storage pool = validationPools[poolIndex];
if (allowance(owner, msg.sender) < amount) {
amount = allowance(owner, msg.sender);
}
_spendAllowance(owner, msg.sender, amount);
_stakeOnValidationPool(pool, owner, amount, inFavor);
}
/// Accept fee to initiate a validation pool
function initiateValidationPool(
address sender,
string calldata postId,
uint duration,
uint[2] calldata quorum, // [Numerator, Denominator]
@ -104,6 +89,10 @@ contract ValidationPools is Reputation, Forum {
bool callbackOnValidate,
bytes calldata callbackData
) external payable returns (uint poolIndex) {
require(
msg.sender == address(dao),
"Only DAO contract may call initiateValidationPool"
);
require(msg.value > 0, "Fee is required to initiate validation pool");
require(duration >= minDuration, "Duration is too short");
require(duration <= maxDuration, "Duration is too long");
@ -114,69 +103,77 @@ contract ValidationPools is Reputation, Forum {
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");
Post storage post = posts[postId];
require(post.authors.length != 0, "Target post not found");
poolIndex = validationPoolCount++;
ValidationPool storage pool = validationPools[poolIndex];
pool.sender = msg.sender;
pool.postId = postId;
pool.fee = msg.value;
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.endTime = block.timestamp + duration;
pool.id = poolIndex;
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
_mint(address(this), msg.value);
pool.minted = msg.value;
emit ValidationPoolInitiated(poolIndex);
dao.mint(address(dao), msg.value);
pool.props.minted = msg.value;
dao.emitValidationPoolInitiated(poolIndex);
}
/// 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"
);
ValidationPool storage pool = validationPools[poolIndex];
require(pool.resolved == false, "Pool is already resolved");
require(pool.props.resolved == false, "Pool is already resolved");
uint stakedFor;
uint stakedAgainst;
ValidationPoolStake 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) {
stakedFor += s.amount;
} else {
stakedAgainst += s.amount;
}
}
stakedFor += pool.minted / 2;
stakedAgainst += pool.minted / 2;
if (pool.minted % 2 != 0) {
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.endTime ||
stakedFor + stakedAgainst == totalSupply(),
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) <=
totalSupply() * pool.params.quorum[0]
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.fee);
pool.resolved = true;
emit ValidationPoolResolved(poolIndex, false, false);
// payable(pool.sender).transfer(pool.props.fee);
pool.props.resolved = true;
dao.emitValidationPoolResolved(poolIndex, false, false);
// Callback if requested
if (pool.callbackOnValidate) {
IOnValidate(pool.sender).onValidate(
dao.onValidate(
pool.sender,
votePasses,
false,
stakedFor,
@ -193,9 +190,9 @@ contract ValidationPools is Reputation, Forum {
votePasses =
stakedFor * pool.params.winRatio[1] >=
(stakedFor + stakedAgainst) * pool.params.winRatio[0];
pool.resolved = true;
pool.outcome = votePasses;
emit ValidationPoolResolved(poolIndex, votePasses, true);
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
@ -209,10 +206,10 @@ contract ValidationPools is Reputation, Forum {
// Losing stake
uint amount = (s.amount * pool.params.bindingPercent) / 100;
if (pool.params.redistributeLosingStakes) {
_update(s.sender, address(this), amount);
dao.update(s.sender, address(dao), amount);
totalRewards += amount;
} else {
_burn(s.sender, amount);
dao.burn(s.sender, amount);
}
}
}
@ -221,7 +218,7 @@ contract ValidationPools is Reputation, Forum {
// 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.minted / 2;
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++) {
@ -234,26 +231,24 @@ contract ValidationPools is Reputation, Forum {
uint reward = (((totalRewards * s.amount) /
amountFromWinners) * pool.params.bindingPercent) / 100;
totalAllocated += reward;
_update(address(this), s.sender, 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.minted % 2 != 0) {
if (pool.props.minted % 2 != 0) {
// We staked the odd remainder in favor of the post, on behalf of the author.
remainder += 1;
}
// Transfer REP to the forum instead of to the author directly
_propagateReputation(
pool.postId,
int(pool.minted / 2 + remainder),
false,
0
dao.propagateReputation(
pool.props.postId,
int(pool.props.minted / 2 + remainder)
);
} else {
// If vote does not pass, divide the losing stake among the winners
totalRewards += pool.minted;
totalRewards += pool.props.minted;
for (uint i = 0; i < pool.stakeCount; i++) {
s = pool.stakes[i];
if (
@ -262,25 +257,21 @@ contract ValidationPools is Reputation, Forum {
) {
// Winning stake
uint reward = (((totalRewards * s.amount) /
(amountFromWinners - pool.minted / 2)) *
(amountFromWinners - pool.props.minted / 2)) *
pool.params.bindingPercent) / 100;
totalAllocated += reward;
_update(address(this), s.sender, reward);
dao.update(address(dao), s.sender, reward);
}
}
}
// Distribute fee proportionately among all reputation holders
for (uint i = 0; i < memberCount; i++) {
address member = members[i];
uint share = (pool.fee * balanceOf(member)) / totalSupply();
// TODO: For efficiency this could be modified to hold the funds for recipients to withdraw
payable(member).transfer(share);
}
dao.distributeFeeAmongMembers{value: pool.props.fee}();
// Callback if requested
if (pool.callbackOnValidate) {
IOnValidate(pool.sender).onValidate(
dao.onValidate(
pool.sender,
votePasses,
true,
stakedFor,

View File

@ -3,24 +3,115 @@ pragma solidity ^0.8.24;
import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "./Reputation.sol";
import "./ValidationPools.sol";
import "./Bench.sol";
import "./Forum.sol";
import "../interfaces/IAcceptAvailability.sol";
import "../interfaces/IOnValidate.sol";
contract DAO is Reputation, Forum, ValidationPools {
// constructor(DAO importFrom) {
// if (address(importFrom) != address(0)) {
// for (uint i = 0; i < importFrom.memberCount(); i++) {
// members[i] = importFrom.members(i);
// isMember[members[i]] = true;
// _mint(members[i], importFrom.balanceOf(members[i]));
// }
// for (uint i = 0; i < importFrom.postCount(); i++) {
// string memory postId = importFrom.postIds(i);
// Post memory post = importFrom.posts(postId);
// }
// }
// }
import "hardhat/console.sol";
contract DAO {
Reputation rep;
Forum forum;
Bench bench;
mapping(uint => address) public members;
uint public memberCount;
mapping(address => bool) public isMember;
event ValidationPoolInitiated(uint poolIndex);
event ValidationPoolResolved(
uint poolIndex,
bool votePasses,
bool quorumMet
);
event PostAdded(string id);
constructor(Reputation reputation_, Forum forum_, Bench bench_) {
rep = reputation_;
forum = forum_;
bench = bench_;
rep.registerDAO(this);
forum.registerDAO(this);
bench.registerDAO(this);
}
function emitValidationPoolInitiated(uint poolIndex) public {
emit ValidationPoolInitiated(poolIndex);
}
function emitValidationPoolResolved(
uint poolIndex,
bool votePasses,
bool quorumMet
) public {
emit ValidationPoolResolved(poolIndex, votePasses, quorumMet);
}
function emitPostAdded(string memory id) public {
emit PostAdded(id);
}
function update(address from, address to, uint256 value) public {
require(
msg.sender == address(forum) || msg.sender == address(bench),
"Only DAO core contracts may call update"
);
rep.update(from, to, value);
}
function mint(address account, uint256 value) public {
require(
msg.sender == address(forum) || msg.sender == address(bench),
"Only DAO core contracts may call mint"
);
rep.mint(account, value);
}
function burn(address account, uint256 value) public {
require(
msg.sender == address(forum) || msg.sender == address(bench),
"Only DAO core contracts may call burn"
);
rep.burn(account, value);
}
function registerMember(address account) public {
require(
msg.sender == address(forum) || msg.sender == address(bench),
"Only DAO core contracts may call registerMember"
);
if (!isMember[account]) {
members[memberCount++] = account;
isMember[account] = true;
}
}
function balanceOf(address account) public view returns (uint256) {
return rep.balanceOf(account);
}
function allowance(
address owner,
address spender
) public view returns (uint256) {
return rep.allowance(owner, spender);
}
function spendAllowance(
address owner,
address spender,
uint256 value
) public {
return rep.spendAllowance(owner, spender, value);
}
function forwardAllowance(
address owner,
address to,
uint256 amount
) public {
return rep.forwardAllowance(owner, to, amount);
}
/// Authorize a contract to transfer REP, and call that contract's acceptAvailability method
function stakeAvailability(
@ -28,10 +119,157 @@ contract DAO is Reputation, Forum, ValidationPools {
uint256 value,
uint duration
) external returns (bool) {
_approve(msg.sender, to, allowance(msg.sender, to) + value);
rep.approve(msg.sender, to, allowance(msg.sender, to) + value);
IAcceptAvailability(to).acceptAvailability(msg.sender, value, duration);
return true;
}
function totalSupply() public view returns (uint256) {
return rep.totalSupply();
}
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++) {
address member = members[i];
uint share;
if (i < memberCount - 1) {
share = (msg.value * balanceOf(member)) / totalSupply();
allocated += share;
} else {
// Due to rounding, give the remainder to the last member
share = msg.value - allocated;
}
// TODO: For efficiency this could be modified to hold the funds for recipients to withdraw
payable(member).transfer(share);
}
}
function validationPools(
uint poolIndex
)
public
view
returns (
uint id,
address sender,
uint stakeCount,
ValidationPoolParams memory params,
ValidationPoolProps memory props,
bool callbackOnValidate,
bytes memory callbackData
)
{
return bench.validationPools(poolIndex);
}
function validationPoolCount() public view returns (uint) {
return bench.validationPoolCount();
}
function initiateValidationPool(
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) {
return
bench.initiateValidationPool{value: msg.value}(
msg.sender,
postId,
duration,
quorum,
winRatio,
bindingPercent,
redistributeLosingStakes,
callbackOnValidate,
callbackData
);
}
function stakeOnValidationPool(
uint poolIndex,
uint256 amount,
bool inFavor
) public {
require(
balanceOf(msg.sender) >= amount,
"Insufficient REP balance to cover stake"
);
// TODO: Encumber tokens
bench.stakeOnValidationPool(poolIndex, msg.sender, amount, inFavor);
}
/// Accept reputation stakes toward a validation pool
function delegatedStakeOnValidationPool(
uint poolIndex,
address owner,
uint256 amount,
bool inFavor
) public {
if (allowance(owner, msg.sender) < amount) {
amount = allowance(owner, msg.sender);
}
spendAllowance(owner, msg.sender, amount);
bench.stakeOnValidationPool(poolIndex, owner, amount, inFavor);
}
function evaluateOutcome(uint poolIndex) public returns (bool) {
return bench.evaluateOutcome(poolIndex);
}
function onValidate(
address target,
bool votePasses,
bool quorumMet,
uint stakedFor,
uint stakedAgainst,
bytes calldata callbackData
) public {
require(
msg.sender == address(forum) || msg.sender == address(bench),
"Only DAO core contracts may call onValidate"
);
IOnValidate(target).onValidate(
votePasses,
quorumMet,
stakedFor,
stakedAgainst,
callbackData
);
}
function addPost(
Author[] calldata authors,
string calldata postId,
Citation[] calldata citations
) public {
forum.addPost(msg.sender, authors, postId, citations);
}
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 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,7 +1,7 @@
// SPDX-License-Identifier: Unlicense
pragma solidity ^0.8.24;
import "./Reputation.sol";
import "./DAO.sol";
struct Citation {
int weightPPM;
@ -22,11 +22,12 @@ struct Post {
// TODO: timestamp
}
contract Forum is Reputation {
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);
@ -34,11 +35,24 @@ contract Forum is Reputation {
// 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,
Citation[] calldata citations
) 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);
@ -47,15 +61,15 @@ contract Forum is Reputation {
post.authors.length == 0,
"A post with this postId already exists"
);
post.sender = msg.sender;
post.sender = sender;
post.id = postId;
uint authorTotalWeightPercent;
uint authorTotalWeightPPM;
for (uint i = 0; i < authors.length; i++) {
authorTotalWeightPercent += authors[i].weightPPM;
authorTotalWeightPPM += authors[i].weightPPM;
post.authors.push(authors[i]);
}
require(
authorTotalWeightPercent == 1000000,
authorTotalWeightPPM == 1000000,
"Author weights must sum to 1000000"
);
for (uint i = 0; i < citations.length; i++) {
@ -84,7 +98,7 @@ contract Forum is Reputation {
totalCitationWeightNeg >= -1000000,
"Sum of negative citations must be >= -1000000"
);
emit PostAdded(postId);
dao.emitPostAdded(postId);
}
function getPostAuthors(
@ -108,7 +122,7 @@ contract Forum is Reputation {
outboundAmount >= 0,
"Leaching from incinerator is forbidden"
);
_burn(address(this), uint(outboundAmount));
dao.burn(address(dao), uint(outboundAmount));
return outboundAmount;
}
int balanceToOutbound = _edgeBalances[postId][citation.targetPostId];
@ -123,7 +137,7 @@ contract Forum is Reputation {
: -balanceToOutbound;
}
}
int refund = _propagateReputation(
int refund = propagateReputation(
citation.targetPostId,
outboundAmount,
initialNegative || (depth == 0 && citation.weightPPM < 0),
@ -140,11 +154,7 @@ contract Forum is Reputation {
int allocated;
for (uint i = 0; i < post.authors.length; i++) {
address authorAddress = post.authors[i].authorAddress;
if (!isMember[authorAddress]) {
members[memberCount++] = authorAddress;
isMember[authorAddress] = true;
}
dao.registerMember(post.authors[i].authorAddress);
}
for (uint i = 0; i < post.authors.length; i++) {
Author memory author = post.authors[i];
@ -157,33 +167,34 @@ contract Forum is Reputation {
share = amount - allocated;
}
if (share > 0) {
_update(address(this), author.authorAddress, uint(share));
if (!isMember[author.authorAddress]) {
members[memberCount++] = author.authorAddress;
isMember[author.authorAddress] = true;
}
} else if (balanceOf(author.authorAddress) < uint(-share)) {
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(balanceOf(author.authorAddress));
_update(
refund += share + int(dao.balanceOf(author.authorAddress));
dao.update(
author.authorAddress,
address(this),
balanceOf(author.authorAddress)
address(dao),
dao.balanceOf(author.authorAddress)
);
} else {
_update(author.authorAddress, address(this), uint(-share));
dao.update(author.authorAddress, address(dao), uint(-share));
}
}
}
function _propagateReputation(
function propagateReputation(
string memory postId,
int amount,
bool initialNegative,
uint depth
) internal returns (int refundToInbound) {
) public returns (int refundToInbound) {
require(
msg.sender == address(dao) || msg.sender == address(this),
"Only DAO contract may call propagateReputation"
);
if (depth >= depthLimit) {
return amount;
}

View File

@ -2,11 +2,36 @@
pragma solidity ^0.8.24;
import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "./DAO.sol";
contract Reputation is ERC20("Reputation", "REP") {
mapping(uint => address) public members;
uint public memberCount;
mapping(address => bool) public isMember;
DAO dao;
function registerDAO(DAO dao_) external {
require(
address(dao) == address(0),
"A DAO has already been registered"
);
dao = dao_;
}
function update(address from, address to, uint256 value) public {
require(
msg.sender == address(dao),
"Only DAO contract may call update"
);
_update(from, to, value);
}
function mint(address account, uint256 value) public {
require(msg.sender == address(dao), "Only DAO contract may call mint");
_mint(account, value);
}
function burn(address account, uint256 value) public {
require(msg.sender == address(dao), "Only DAO contract may call burn");
_burn(account, value);
}
function decimals() public pure override returns (uint8) {
return 9;
@ -30,7 +55,31 @@ contract Reputation is ERC20("Reputation", "REP") {
address to,
uint256 amount
) public {
require(
msg.sender == address(dao),
"Only DAO contract may call spendAllowance"
);
_spendAllowance(owner, msg.sender, amount);
_approve(owner, to, allowance(owner, to) + amount);
}
function spendAllowance(
address owner,
address spender,
uint256 value
) public {
require(
msg.sender == address(dao),
"Only DAO contract may call spendAllowance"
);
_spendAllowance(owner, spender, value);
}
function approve(address owner, address spender, uint256 value) public {
require(
msg.sender == address(dao),
"Only DAO contract may call update"
);
_approve(owner, spender, value);
}
}

View File

@ -8,5 +8,5 @@ interface IOnValidate {
uint stakedFor,
uint stakedAgainst,
bytes calldata callbackData
) external returns (uint);
) external;
}

View File

@ -1,10 +1,8 @@
const { ethers } = require('hardhat');
const { execSync } = require('child_process');
const { getContractAddressByNetworkName } = require('./contract-config');
const readFromApi = require('./util/read-from-api');
const network = process.env.HARDHAT_NETWORK;
let currentVersionProposalId;
let dao;
let work1;
@ -16,20 +14,6 @@ let posts;
let proposalsContract;
let proposals;
const getCurrentVersion = () => {
const currentCommit = execSync('git rev-parse HEAD');
return currentCommit.toString();
};
const fetchCurrentVersionProposal = async () => {
// const p = await proposalsContract.
};
const getLatestVersion = () => {
const latestVersion = 'TBD';
return latestVersion;
};
const fetchReputation = async () => {
reputation = await dao.balanceOf(account);
console.log(`reputation: ${reputation}`);
@ -75,22 +59,6 @@ const fetchValidationPools = async () => {
await Promise.all(promises);
};
const fetchProposal = async (proposalIndex) => {
const proposal = await proposalsContract.proposals(proposalIndex);
proposals[proposalIndex] = proposal;
};
const fetchProposals = async () => {
const count = await proposalsContract.proposalCount();
console.log(`proposal count: ${count}`);
const promises = [];
proposals = [];
for (let i = 0; i < count; i += 1) {
promises.push(fetchProposal(i));
}
await Promise.all(promises);
};
const initialize = async () => {
const getContract = (name) => ethers.getContractAt(
name,
@ -106,12 +74,11 @@ const initialize = async () => {
posts = [];
await fetchReputation();
await fetchValidationPools();
await fetchProposals();
};
const poolIsActive = (pool) => {
if (new Date() >= new Date(Number(pool.endTime) * 1000)) return false;
if (pool.resolved) return false;
if (new Date() >= new Date(Number(pool.props.endTime) * 1000)) return false;
if (pool.props.resolved) return false;
return true;
};
@ -138,8 +105,8 @@ const poolIsProposal = (pool) => pool.sender === getContractAddressByNetworkName
const getPoolStatus = (pool) => {
if (poolIsActive(pool)) return 'Active';
if (!pool.resolved) return 'Ready to Evaluate';
if (pool.outcome) return 'Accepted';
if (!pool.props.resolved) return 'Ready to Evaluate';
if (pool.props.outcome) return 'Accepted';
return 'Rejected';
};
@ -179,8 +146,6 @@ const printPool = (pool) => {
};
async function main() {
console.log('Current version:', getCurrentVersion());
await initialize();
validationPools.forEach(printPool);

View File

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

View File

@ -1,10 +1,10 @@
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');
async function main() {
await deployContract('DAO', [], true);
await deployCoreContracts();
await deployDAOContract('Rollup');
await deployDAOContract('Proposals');
await deployWorkContract('Work1');

View File

@ -0,0 +1,18 @@
require('dotenv').config();
const deployContract = require('./deploy-contract');
const contractAddresses = require('../../contract-addresses.json');
const network = process.env.HARDHAT_NETWORK;
const deployCoreContracts = async () => {
await deployContract('Reputation', [], true);
await deployContract('Forum', [], true);
await deployContract('Bench', [], true);
await deployContract('DAO', [
contractAddresses[network].Reputation,
contractAddresses[network].Forum,
contractAddresses[network].Bench,
], true);
};
module.exports = deployCoreContracts;

View File

@ -4,12 +4,12 @@ const {
} = require('@nomicfoundation/hardhat-toolbox/network-helpers');
const { expect } = require('chai');
const { ethers } = require('hardhat');
const deployDAO = require('./util/deploy-dao');
describe('Forum', () => {
async function deploy() {
const [account1, account2, account3] = await ethers.getSigners();
const DAO = await ethers.getContractFactory('DAO');
const dao = await DAO.deploy();
const { dao } = await deployDAO();
return {
dao, account1, account2, account3,
};
@ -118,7 +118,7 @@ describe('Forum', () => {
await addPost(account2, 'second-content-id', [{ weightPPM: 500000, targetPostId: 'content-id' }]);
await initiateValidationPool({ postId: 'second-content-id' });
const pool = await dao.validationPools(0);
expect(pool.postId).to.equal('second-content-id');
expect(pool.props.postId).to.equal('second-content-id');
await dao.evaluateOutcome(0);
expect(await dao.balanceOf(account1)).to.equal(50);
expect(await dao.balanceOf(account2)).to.equal(50);
@ -135,7 +135,7 @@ describe('Forum', () => {
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.postId).to.equal('second-content-id');
expect(pool.props.postId).to.equal('second-content-id');
await time.increase(POOL_DURATION + 1);
await dao.evaluateOutcome(1);
expect(await dao.balanceOf(account1)).to.equal(50);
@ -157,7 +157,7 @@ describe('Forum', () => {
]);
await initiateValidationPool({ postId: 'third-content-id' });
const pool = await dao.validationPools(1);
expect(pool.postId).to.equal('third-content-id');
expect(pool.props.postId).to.equal('third-content-id');
await time.increase(POOL_DURATION + 1);
await dao.evaluateOutcome(1);
expect(await dao.balanceOf(account1)).to.equal(0);
@ -193,7 +193,7 @@ describe('Forum', () => {
]);
await initiateValidationPool({ postId: 'second-content-id' });
const pool = await dao.validationPools(0);
expect(pool.postId).to.equal('second-content-id');
expect(pool.props.postId).to.equal('second-content-id');
await dao.evaluateOutcome(0);
expect(await dao.balanceOf(account1)).to.equal(10);
expect(await dao.balanceOf(account2)).to.equal(90);
@ -388,7 +388,7 @@ describe('Forum', () => {
// account1 stakes and loses
await initiateValidationPool({ postId: 'content-id' });
await dao.stakeOnValidationPool(1, 25, true);
await dao.connect(account2).stakeOnValidationPool(1, 60, false);
await dao.connect(account2).stakeOnValidationPool(1, 50, false);
await time.increase(POOL_DURATION + 1);
await dao.evaluateOutcome(1);
expect(await dao.balanceOf(account1)).to.equal(25);

View File

@ -4,6 +4,7 @@ const {
} = require('@nomicfoundation/hardhat-toolbox/network-helpers');
const { expect } = require('chai');
const { ethers } = require('hardhat');
const deployDAO = require('./util/deploy-dao');
describe('Onboarding', () => {
const PRICE = 100;
@ -12,8 +13,7 @@ describe('Onboarding', () => {
// Contracts are deployed using the first signer/account by default
const [account1, account2] = await ethers.getSigners();
const DAO = await ethers.getContractFactory('DAO');
const dao = await DAO.deploy();
const { dao } = await deployDAO();
const Proposals = await ethers.getContractFactory('Proposals');
const proposals = await Proposals.deploy(dao.target);
const Onboarding = await ethers.getContractFactory('Onboarding');
@ -78,8 +78,8 @@ describe('Onboarding', () => {
expect(postAuthors[0].weightPPM).to.equal(1000000);
expect(postAuthors[0].authorAddress).to.equal(account1);
const pool = await dao.validationPools(1);
expect(pool.postId).to.equal('evidence-content-id');
expect(pool.fee).to.equal(PRICE * 0.9);
expect(pool.props.postId).to.equal('evidence-content-id');
expect(pool.props.fee).to.equal(PRICE * 0.9);
expect(pool.sender).to.equal(onboarding.target);
});
@ -131,10 +131,10 @@ describe('Onboarding', () => {
expect(postAuthors[0].weightPPM).to.equal(1000000);
expect(postAuthors[0].authorAddress).to.equal(account2);
const pool = await dao.validationPools(2);
expect(pool.postId).to.equal('req-content-id');
expect(pool.fee).to.equal(PRICE * 0.1);
expect(pool.props.postId).to.equal('req-content-id');
expect(pool.props.fee).to.equal(PRICE * 0.1);
expect(pool.sender).to.equal(onboarding.target);
expect(pool.fee);
expect(pool.props.fee);
});
it('if the first validation pool is rejected it should not trigger a second pool', async () => {

View File

@ -5,14 +5,14 @@ const {
const { expect } = require('chai');
const { ethers } = require('hardhat');
const { beforeEach } = require('mocha');
const deployDAO = require('./util/deploy-dao');
describe('Proposal', () => {
async function deploy() {
// Contracts are deployed using the first signer/account by default
const [account1, account2] = await ethers.getSigners();
const DAO = await ethers.getContractFactory('DAO');
const dao = await DAO.deploy();
const { dao } = await deployDAO();
const Proposals = await ethers.getContractFactory('Proposals');
const proposals = await Proposals.deploy(dao.target);
@ -223,7 +223,7 @@ describe('Proposal', () => {
afterEach(async () => {
const pool = await dao.validationPools(3);
expect(pool.resolved).to.be.true;
expect(pool.props.resolved).to.be.true;
});
it('proposal dies if it fails to meet quorum', async () => {
@ -309,7 +309,7 @@ describe('Proposal', () => {
afterEach(async () => {
const pool = await dao.validationPools(4);
expect(pool.resolved).to.be.true;
expect(pool.props.resolved).to.be.true;
});
it('proposal dies if it fails to meet quorum', async () => {

View File

@ -4,13 +4,15 @@ const {
} = require('@nomicfoundation/hardhat-toolbox/network-helpers');
const { expect } = require('chai');
const { ethers } = require('hardhat');
const deployDAO = require('./util/deploy-dao');
describe('Validation Pools', () => {
async function deploy() {
const [account1, account2] = await ethers.getSigners();
const DAO = await ethers.getContractFactory('DAO');
const dao = await DAO.deploy();
return { dao, account1, account2 };
const { dao } = await deployDAO();
return {
dao, account1, account2,
};
}
let dao;
let account1;
@ -37,7 +39,9 @@ describe('Validation Pools', () => {
);
beforeEach(async () => {
({ dao, account1, account2 } = await loadFixture(deploy));
({
dao, account1, account2,
} = await loadFixture(deploy));
await dao.addPost([{ weightPPM: 1000000, authorAddress: account1 }], 'content-id', []);
const init = () => initiateValidationPool({ fee: POOL_FEE });
await expect(init()).to.emit(dao, 'ValidationPoolInitiated').withArgs(0);
@ -88,8 +92,8 @@ describe('Validation Pools', () => {
const pool = await dao.validationPools(0);
expect(pool).to.exist;
expect(pool.params.duration).to.equal(POOL_DURATION);
expect(pool.postId).to.equal('content-id');
expect(pool.resolved).to.be.false;
expect(pool.props.postId).to.equal('content-id');
expect(pool.props.resolved).to.be.false;
expect(pool.sender).to.equal(account1);
});
});
@ -129,7 +133,17 @@ describe('Validation Pools', () => {
expect(await dao.balanceOf(account1)).to.equal(200);
expect(await dao.balanceOf(dao.target)).to.equal(0);
const pool = await dao.validationPools(1);
expect(pool.outcome).to.be.false;
expect(pool.props.outcome).to.be.false;
});
it('should not be able to stake more REP than the sender owns', async () => {
await expect(dao.stakeOnValidationPool(1, 200, true)).to.be.revertedWith('Insufficient REP balance to cover stake');
});
});
describe('Delegated stake', () => {
it('should stake the lesser of the allowed amount or the owner\'s remaining balance', async () => {
// TODO: owner delegates stake and then loses rep
});
});
@ -156,8 +170,8 @@ describe('Validation Pools', () => {
expect(await dao.memberCount()).to.equal(1);
expect(await dao.balanceOf(account1)).to.equal(100);
const pool = await dao.validationPools(0);
expect(pool.resolved).to.be.true;
expect(pool.outcome).to.be.true;
expect(pool.props.resolved).to.be.true;
expect(pool.props.outcome).to.be.true;
});
it('should not be able to evaluate outcome more than once', async () => {

View File

@ -4,6 +4,7 @@ const {
} = require('@nomicfoundation/hardhat-toolbox/network-helpers');
const { expect } = require('chai');
const { ethers } = require('hardhat');
const deployDAO = require('./util/deploy-dao');
describe('Work1', () => {
const WORK1_PRICE = 100;
@ -12,8 +13,7 @@ describe('Work1', () => {
// Contracts are deployed using the first signer/account by default
const [account1, account2] = await ethers.getSigners();
const DAO = await ethers.getContractFactory('DAO');
const dao = await DAO.deploy();
const { dao } = await deployDAO();
const Proposals = await ethers.getContractFactory('Proposals');
const proposals = await Proposals.deploy(dao.target);
const Work1 = await ethers.getContractFactory('Work1');
@ -223,9 +223,9 @@ describe('Work1', () => {
expect(postAuthors[0].weightPPM).to.equal(1000000);
expect(postAuthors[0].authorAddress).to.equal(account1);
const pool = await dao.validationPools(1);
expect(pool.fee).to.equal(WORK1_PRICE);
expect(pool.props.fee).to.equal(WORK1_PRICE);
expect(pool.sender).to.equal(work1.target);
expect(pool.postId).to.equal('evidence-content-id');
expect(pool.props.postId).to.equal('evidence-content-id');
expect(pool.stakeCount).to.equal(1);
await time.increase(86401);
await expect(dao.evaluateOutcome(1)).to.emit(dao, 'ValidationPoolResolved').withArgs(1, true, true);

View File

@ -0,0 +1,24 @@
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 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,
);
return {
dao,
reputation,
forum,
bench,
};
};
module.exports = deployDAO;

View File

@ -1,11 +1,14 @@
{
"localhost": {
"DAO": "0x87933c1e51FdF52C58ee54348a9372bbDeE9A8Dc",
"Work1": "0x215078c5cf21ffB79Ee14Cf09156B94a11b7340f",
"Onboarding": "0x3c2820D27e7470075d16856D7D555FD5011451Ec",
"Proposals": "0xCd5881DB1aa6b86283a9c5660FaB65C989cf2721",
"Rollup": "0x8611676563EBcd69dC52E5829bF2914A957398C3",
"Work2": "0xC6BF1b68311e891D2BF41E4A3CB350a403831Ccd"
"DAO": "0x3287061aDCeE36C1aae420a06E4a5EaE865Fe3ce",
"Work1": "0x76Dfe9F47f06112a1b78960bf37d87CfbB6D6133",
"Onboarding": "0xd2845aE812Ee42cF024fB4C55c052365792aBd78",
"Proposals": "0x8688E736D0D72161db4D25f68EF7d0EE4856ba19",
"Rollup": "0x8BDA04936887cF11263B87185E4D19e8158c6296",
"Work2": "0xf15aCe29E5e3e4bb31FCddF2C65DF7C440449a57",
"Reputation": "0xC0f00E5915F9abE6476858fD1961EAf79395ea64",
"Forum": "0x3734B0944ea37694E85AEF60D5b256d19EDA04be",
"Bench": "0x71cb20D63576a0Fa4F620a2E96C73F82848B09e1"
},
"sepolia": {
"DAO": "0x8e5bd58B2ca8910C5F9be8de847d6883B15c60d2",

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -211,9 +211,9 @@ function MainTabs() {
{validationPools.filter((x) => !!x).map((pool) => (
<tr key={pool.id}>
<td>{pool.id.toString()}</td>
<td>{pool.postId}</td>
<td>{pool.props.postId}</td>
<td>{getAddressName(chainId, pool.sender)}</td>
<td>{pool.fee.toString()}</td>
<td>{pool.props.fee.toString()}</td>
<td>
{pool.params.bindingPercent.toString()}
%
@ -222,11 +222,11 @@ function MainTabs() {
<td>{`${pool.params.winRatio[0].toString()}/${pool.params.winRatio[1].toString()}`}</td>
<td>{pool.params.redistributeLosingStakes.toString()}</td>
<td>{pool.params.duration.toString()}</td>
<td>{new Date(Number(pool.endTime) * 1000).toLocaleString()}</td>
<td>{new Date(Number(pool.props.endTime) * 1000).toLocaleString()}</td>
<td>{pool.stakeCount.toString()}</td>
<td>{pool.status}</td>
<td>
{!pool.resolved && reputation > 0 && pool.timeRemaining > 0 && (
{!pool.props.resolved && reputation > 0 && pool.timeRemaining > 0 && (
<>
<Button onClick={() => stakeHalfInFavor(pool.id)}>
Stake 1/2 REP
@ -238,7 +238,7 @@ function MainTabs() {
{' '}
</>
)}
{!pool.resolved && (pool.timeRemaining <= 0 || !reputation) && (
{!pool.props.resolved && (pool.timeRemaining <= 0 || !reputation) && (
<Button onClick={() => evaluateOutcome(pool.id)}>
Evaluate Outcome
</Button>

View File

@ -93,14 +93,14 @@ function MainContextProvider({ children }) {
const fetchValidationPool = useCallback(async (poolIndex) => {
const getPoolStatus = (pool) => {
if (pool.resolved) {
return pool.outcome ? 'Accepted' : 'Rejected';
if (pool.props.resolved) {
return pool.props.outcome ? 'Accepted' : 'Rejected';
}
return pool.timeRemaining > 0 ? 'In Progress' : 'Ready to Evaluate';
};
const pool = await DAORef.current.methods.validationPools(poolIndex).call();
pool.id = Number(pool.id);
pool.timeRemaining = new Date(Number(pool.endTime) * 1000) - new Date();
pool.timeRemaining = new Date(Number(pool.props.endTime) * 1000) - new Date();
pool.status = getPoolStatus(pool);
dispatchValidationPool({ type: 'update', item: pool });