From 1306fe2414c35228b634e9e986aec99eb61928eb Mon Sep 17 00:00:00 2001 From: Ladd Hoffman Date: Tue, 26 Mar 2024 21:32:41 -0500 Subject: [PATCH] Proposals contract: attestation --- ethereum/contracts/DAO.sol | 2 - ethereum/contracts/Proposal.sol | 56 +++++++++++++++--- ethereum/test/Proposals.js | 101 ++++++++++++++++++++++++++++++++ 3 files changed, 148 insertions(+), 11 deletions(-) create mode 100644 ethereum/test/Proposals.js diff --git a/ethereum/contracts/DAO.sol b/ethereum/contracts/DAO.sol index eaf53b9..74b56c4 100644 --- a/ethereum/contracts/DAO.sol +++ b/ethereum/contracts/DAO.sol @@ -218,13 +218,11 @@ contract DAO is ERC20("Reputation", "REP") { ? ((s.amount * amountFromLosers) / amountFromWinners) * (bindingPercent / 100) : 0; - uint balance = balanceOf(address(this)); _update(address(this), s.sender, s.amount + reward); totalAllocated += reward; } else { // Losing stake uint refund = (s.amount * (100 - bindingPercent)) / 100; - uint balance = balanceOf(address(this)); if (refund > 0) { _update(address(this), s.sender, refund); } diff --git a/ethereum/contracts/Proposal.sol b/ethereum/contracts/Proposal.sol index cd880fc..b458621 100644 --- a/ethereum/contracts/Proposal.sol +++ b/ethereum/contracts/Proposal.sol @@ -2,6 +2,7 @@ pragma solidity ^0.8.24; import "./DAO.sol"; +import "hardhat/console.sol"; contract Proposals is DAOContract { struct Attestation { @@ -16,27 +17,44 @@ contract Proposals is DAOContract { Referendum100, Closed } + + struct Referendum { + uint duration; + uint poolIndex; + } + struct Proposal { address sender; - string contentId; + uint fee; + uint feeRemaining; + uint postIndex; uint startTime; Stage stage; mapping(uint => Attestation) attestations; uint attestationCount; + Referendum[3] referenda; } - mapping(uint => Proposal) proposals; - uint proposalCount; + mapping(uint => Proposal) public proposals; + uint public proposalCount; constructor(DAO dao) DAOContract(dao) {} function propose( - string calldata contentId - ) external returns (uint proposalIndex) { + uint postIndex, + uint referendum0Duration, + uint referendum1Duration, + uint referendum100Duration + ) external payable returns (uint proposalIndex) { proposalIndex = proposalCount++; Proposal storage proposal = proposals[proposalIndex]; + proposal.postIndex = postIndex; proposal.startTime = block.timestamp; - proposal.contentId = contentId; + proposal.referenda[0].duration = referendum0Duration; + proposal.referenda[1].duration = referendum1Duration; + proposal.referenda[2].duration = referendum100Duration; + proposal.fee = msg.value; + proposal.feeRemaining = proposal.fee; } function attest(uint proposalIndex, uint amount) external { @@ -55,6 +73,12 @@ contract Proposals is DAOContract { attestation.amount = amount; } + // todo onValidate() { + + // This callback will get proposalIndex + + // todo } + function evaluateAttestation(uint proposalIndex) external returns (bool) { Proposal storage proposal = proposals[proposalIndex]; require( @@ -66,16 +90,30 @@ contract Proposals is DAOContract { totalAttestation += proposal.attestations[i].amount; } bool meetsAttestation = 10 * totalAttestation >= dao.totalSupply(); + bool expired = block.timestamp > proposal.startTime + 365 days; if (!meetsAttestation) { - if (block.timestamp > proposal.startTime + 365 days) { + if (expired) { proposal.stage = Stage.Closed; return false; } return false; } - // Initiate validation pool proposal.stage = Stage.Referendum0; - // TODO: make referendum0 duration a parameter + uint thisFee = proposal.fee / 3; + proposal.feeRemaining -= thisFee; + proposal.referenda[0].poolIndex = dao.initiateValidationPool{ + value: thisFee + }( + proposal.postIndex, // uint postIndex, + proposal.referenda[0].duration, // uint duration, + 1, // uint quorumNumerator, + 3, // uint quorumDenominator, + 0, // uint bindingPercent, + false, // bool redistributeLosingStakes, + false, // TODO bool callbackOnValidate : true, + "" // TODO bytes calldata callbackData : This should probably be proposalIndex + ); + return true; } } diff --git a/ethereum/test/Proposals.js b/ethereum/test/Proposals.js new file mode 100644 index 0000000..333c44f --- /dev/null +++ b/ethereum/test/Proposals.js @@ -0,0 +1,101 @@ +const { + time, + loadFixture, +} = require('@nomicfoundation/hardhat-toolbox/network-helpers'); +const { expect } = require('chai'); +const { ethers } = require('hardhat'); +const { beforeEach } = require('mocha'); + +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 Proposals = await ethers.getContractFactory('Proposals'); + const proposals = await Proposals.deploy(dao.target); + + await dao.addPost(account1, 'some-content-id'); + await dao.addPost(account2, 'some-other-content-id'); + const callbackData = ethers.AbiCoder.defaultAbiCoder().encode([], []); + await dao.initiateValidationPool(0, 60, 1, 3, 100, true, false, callbackData, { value: 100 }); + await dao.initiateValidationPool(1, 60, 1, 3, 100, true, false, callbackData, { value: 100 }); + await time.increase(61); + await dao.evaluateOutcome(0); + await dao.evaluateOutcome(1); + + return { + dao, proposals, account1, account2, + }; + } + + it('Should deploy', async () => { + const { dao, proposals, account1 } = await loadFixture(deploy); + expect(dao).to.exist; + expect(proposals).to.exist; + expect(await dao.memberCount()).to.equal(2); + expect(await dao.balanceOf(account1)).to.equal(100); + expect(await dao.totalSupply()).to.equal(200); + expect(await proposals.proposalCount()).to.equal(0); + }); + + describe('Attestation', () => { + let dao; + let proposals; + let account1; + // let account2; + let proposal; + + beforeEach(async () => { + ({ + dao, + proposals, + account1, + // account2, + } = await loadFixture(deploy)); + + console.log('postCount', await dao.postCount()); + await dao.addPost(account1, 'proposal-content-id'); + const postIndex = await dao.postCount() - BigInt(1); + const post = await dao.posts(postIndex); + expect(await post.contentId).to.equal('proposal-content-id'); + await proposals.propose(postIndex, 20, 20, 20, { value: 100 }); + expect(await proposals.proposalCount()).to.equal(1); + proposal = await proposals.proposals(0); + expect(proposal.postIndex).to.equal(postIndex); + expect(proposal.stage).to.equal(0); + }); + + it('Can submit a proposal', async () => { + // Nothing to do here -- this just tests our beforeEach + }); + + it('Can attest for a proposal', async () => { + await proposals.connect(account1).attest(0, 50); + // Nonbinding, non-encumbering + expect(await dao.balanceOf(account1)).to.equal(100); + }); + + describe('Evaluate attestation', () => { + it('when threshold is met, advance to referendum 0% binding', async () => { + await proposals.attest(0, 20); + await proposals.evaluateAttestation(0); + proposal = await proposals.proposals(0); + expect(proposal.stage).to.equal(1); + }); + + it('when threshold is not met, and duration has not elapsed, do nothing', async () => { + await proposals.evaluateAttestation(0); + expect(proposal.stage).to.equal(0); + }); + + it('when threshold is not met, and duration has elapsed, close the proposal', async () => { + await time.increase(365 * 86400 + 1); // 1 year + 1 second + await proposals.evaluateAttestation(0); + proposal = await proposals.proposals(0); + expect(proposal.stage).to.equal(4); // Stage.Closed + }); + }); + }); +});