Proposals: complete VP workflow
Gitea Actions Demo / Explore-Gitea-Actions (push) Failing after 38s Details

This commit is contained in:
Ladd Hoffman 2024-03-27 14:03:57 -05:00
parent 5000ef60fd
commit a8dcbe7a35
6 changed files with 155 additions and 43 deletions

View File

@ -67,7 +67,11 @@ contract DAO is ERC20("Reputation", "REP") {
event PostAdded(uint postIndex);
event ValidationPoolInitiated(uint poolIndex);
event ValidationPoolResolved(uint poolIndex, bool votePasses);
event ValidationPoolResolved(
uint poolIndex,
bool votePasses,
bool quorumMet
);
function addPost(
address author,
@ -184,11 +188,22 @@ contract DAO is ERC20("Reputation", "REP") {
}
}
// Check that quorum is met
require(
1_000_000_000 * (stakedFor + stakedAgainst) >=
totalSupply() * pool.params.quorumPPB,
"Quorum for this pool was not met"
);
if (
1_000_000_000 * (stakedFor + stakedAgainst) <=
totalSupply() * pool.params.quorumPPB
) {
// TODO: refund stakes
// Callback if requested
if (pool.callbackOnValidate) {
IOnValidate(pool.sender).onValidate(
votePasses,
false,
pool.callbackData
);
}
emit ValidationPoolResolved(poolIndex, false, false);
return false;
}
// A tie is resolved in favor of the validation pool.
// This is especially important so that the DAO's first pool can pass,
// when no reputation has yet been minted.
@ -199,7 +214,7 @@ contract DAO is ERC20("Reputation", "REP") {
}
pool.resolved = true;
pool.outcome = votePasses;
emit ValidationPoolResolved(poolIndex, votePasses);
emit ValidationPoolResolved(poolIndex, votePasses, true);
// Value of losing stakes should be distributed among winners, in proportion to their stakes
uint256 amountFromWinners = votePasses ? stakedFor : stakedAgainst;
uint256 amountFromLosers = votePasses ? stakedAgainst : stakedFor;
@ -250,7 +265,11 @@ contract DAO is ERC20("Reputation", "REP") {
}
// Callback if requested
if (pool.callbackOnValidate) {
IOnValidate(pool.sender).onValidate(votePasses, pool.callbackData);
IOnValidate(pool.sender).onValidate(
votePasses,
true,
pool.callbackData
);
}
}

View File

@ -2,5 +2,9 @@
pragma solidity ^0.8.24;
interface IOnValidate {
function onValidate(bool votePasses, bytes calldata callbackData) external;
function onValidate(
bool votePasses,
bool quorumMet,
bytes calldata callbackData
) external;
}

View File

@ -41,14 +41,18 @@ contract Onboarding is WorkContract, IOnValidate {
}
/// Callback to be executed when review pool completes
function onValidate(bool votePasses, bytes calldata callbackData) external {
function onValidate(
bool votePasses,
bool quorumMet,
bytes calldata callbackData
) external {
require(
msg.sender == address(dao),
"onValidate may only be called by the DAO contract"
);
uint requestIndex = abi.decode(callbackData, (uint));
WorkRequest storage request = requests[requestIndex];
if (!votePasses) {
if (!votePasses || !quorumMet) {
// refund the customer the remaining amount
payable(request.customer).transfer(request.fee / 10);
return;

View File

@ -15,29 +15,39 @@ contract Proposals is DAOContract {
Referendum0,
Referendum1,
Referendum100,
Closed
Failed,
Accepted
}
struct Referendum {
uint duration;
uint poolIndex;
uint fee;
}
struct Proposal {
address sender;
uint fee;
uint feeRemaining;
uint postIndex;
uint startTime;
Stage stage;
mapping(uint => Attestation) attestations;
uint attestationCount;
Referendum[3] referenda;
uint[3] retryCount;
}
mapping(uint => Proposal) public proposals;
uint public proposalCount;
event NewProposal(uint proposalIndex);
event ReferendumStarted(uint proposalIndex, uint poolIndex);
event ProposalFailed(uint proposalIndex, string reason);
event ProposalAccepted(uint proposalIndex);
uint[3] referendaBindingPercent = [0, 1, 100];
bool[3] referendaRedistributeLosingStakes = [false, false, true];
constructor(DAO dao) DAOContract(dao) {}
function propose(
@ -54,7 +64,10 @@ contract Proposals is DAOContract {
proposal.referenda[1].duration = referendum1Duration;
proposal.referenda[2].duration = referendum100Duration;
proposal.fee = msg.value;
proposal.feeRemaining = proposal.fee;
proposal.referenda[0].fee = proposal.fee / 3;
proposal.referenda[1].fee = proposal.fee / 3;
proposal.referenda[2].fee = proposal.fee - (proposal.fee * 2) / 3;
emit NewProposal(proposalIndex);
}
function attest(uint proposalIndex, uint amount) external {
@ -73,11 +86,86 @@ contract Proposals is DAOContract {
attestation.amount = amount;
}
// todo onValidate() {
function initiateValidationPool(
uint proposalIndex,
uint referendumIndex
) internal {
uint bindingPercent = referendaBindingPercent[referendumIndex];
bool redistributeLosingStakes = referendaRedistributeLosingStakes[
referendumIndex
];
Proposal storage proposal = proposals[proposalIndex];
uint poolIndex = dao.initiateValidationPool{
value: proposal.referenda[referendumIndex].fee
}(
proposal.postIndex,
proposal.referenda[referendumIndex].duration,
1,
3,
bindingPercent,
redistributeLosingStakes,
true,
abi.encode(proposalIndex)
);
emit ReferendumStarted(proposalIndex, poolIndex);
}
// This callback will get proposalIndex
// todo }
/// Callback to be executed when referenda pools complete
function onValidate(
bool votePasses,
bool quorumMet,
bytes calldata callbackData
) external {
require(
msg.sender == address(dao),
"onValidate may only be called by the DAO contract"
);
uint proposalIndex = abi.decode(callbackData, (uint));
Proposal storage proposal = proposals[proposalIndex];
if (!quorumMet) {
proposal.stage = Stage.Failed;
emit ProposalFailed(proposalIndex, "Quorum not met");
return;
}
if (proposal.stage == Stage.Referendum0) {
if (votePasses) {
proposal.stage = Stage.Referendum1;
} else if (proposal.retryCount[0] >= 3) {
proposal.stage = Stage.Failed;
emit ProposalFailed(proposalIndex, "Retry count exceeded");
} else {
proposal.retryCount[0] += 1;
}
} else if (proposal.stage == Stage.Referendum1) {
if (votePasses) {
proposal.stage = Stage.Referendum100;
} else if (proposal.retryCount[1] >= 3) {
proposal.stage = Stage.Failed;
emit ProposalFailed(proposalIndex, "Retry count exceeded");
} else {
proposal.retryCount[1] += 1;
}
} else if (proposal.stage == Stage.Referendum100) {
// Note that no retries are attempted for referendum 100%
if (votePasses) {
// TODO: The proposal has passed all referenda and should become "law"
// This is an opportunity for some actions to occur
// We should at least emit an event
proposal.stage = Stage.Accepted;
emit ProposalAccepted(proposalIndex);
} else {
proposal.stage = Stage.Failed;
emit ProposalFailed(proposalIndex, "Binding pool was rejected");
}
}
if (proposal.stage == Stage.Referendum0) {
initiateValidationPool(proposalIndex, 0);
} else if (proposal.stage == Stage.Referendum1) {
initiateValidationPool(proposalIndex, 1);
} else if (proposal.stage == Stage.Referendum100) {
initiateValidationPool(proposalIndex, 2);
}
}
function evaluateAttestation(uint proposalIndex) external returns (bool) {
Proposal storage proposal = proposals[proposalIndex];
@ -93,27 +181,24 @@ contract Proposals is DAOContract {
bool expired = block.timestamp > proposal.startTime + 365 days;
if (!meetsAttestation) {
if (expired) {
proposal.stage = Stage.Closed;
// Expired without meeting attestation threshold
proposal.stage = Stage.Failed;
emit ProposalFailed(
proposalIndex,
"Expired without meeting attestation threshold"
);
return false;
}
// Not yet expired, but has not met attestation threshold
return false;
}
// Initiate validation pool
// Attestation threshold is met.
// Note that this may succeed even after expiry
// It can only happen once because the stage advances, and we required it above.
proposal.stage = Stage.Referendum0;
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
);
// Initiate validation pool
initiateValidationPool(proposalIndex, 0);
return true;
}
}

View File

@ -143,7 +143,7 @@ describe('DAO', () => {
expect(await dao.balanceOf(dao.target)).to.equal(110);
time.increase(POOL_DURATION + 1);
console.log('evaluating second pool');
await expect(dao.evaluateOutcome(1)).to.emit(dao, 'ValidationPoolResolved').withArgs(1, true);
await expect(dao.evaluateOutcome(1)).to.emit(dao, 'ValidationPoolResolved').withArgs(1, true, true);
expect(await dao.balanceOf(dao.target)).to.equal(0);
expect(await dao.balanceOf(account1)).to.equal(200);
});
@ -158,7 +158,7 @@ describe('DAO', () => {
expect(await dao.balanceOf(account1)).to.equal(90);
expect(await dao.balanceOf(dao.target)).to.equal(110);
time.increase(POOL_DURATION + 1);
await expect(dao.evaluateOutcome(1)).to.emit(dao, 'ValidationPoolResolved').withArgs(1, false);
await expect(dao.evaluateOutcome(1)).to.emit(dao, 'ValidationPoolResolved').withArgs(1, false, true);
expect(await dao.balanceOf(dao.target)).to.equal(0);
expect(await dao.balanceOf(account1)).to.equal(200);
const pool = await dao.validationPools(1);
@ -174,7 +174,7 @@ describe('DAO', () => {
it('should be able to evaluate outcome after duration has elapsed', async () => {
expect(await dao.balanceOf(dao.target)).to.equal(100);
time.increase(POOL_DURATION + 1);
await expect(dao.evaluateOutcome(0)).to.emit(dao, 'ValidationPoolResolved').withArgs(0, true);
await expect(dao.evaluateOutcome(0)).to.emit(dao, 'ValidationPoolResolved').withArgs(0, true, true);
expect(await dao.memberCount()).to.equal(1);
expect(await dao.balanceOf(account1)).to.equal(100);
const pool = await dao.validationPools(0);
@ -184,7 +184,7 @@ describe('DAO', () => {
it('should not be able to evaluate outcome more than once', async () => {
time.increase(POOL_DURATION + 1);
await expect(dao.evaluateOutcome(0)).to.emit(dao, 'ValidationPoolResolved').withArgs(0, true);
await expect(dao.evaluateOutcome(0)).to.emit(dao, 'ValidationPoolResolved').withArgs(0, true, true);
await expect(dao.evaluateOutcome(0)).to.be.revertedWith('Pool is already resolved');
});
@ -203,21 +203,21 @@ describe('DAO', () => {
await expect(init()).to.emit(dao, 'ValidationPoolInitiated').withArgs(1);
expect(await dao.validationPoolCount()).to.equal(2);
time.increase(POOL_DURATION + 1);
await expect(dao.evaluateOutcome(0)).to.emit(dao, 'ValidationPoolResolved').withArgs(0, true);
await expect(dao.evaluateOutcome(0)).to.emit(dao, 'ValidationPoolResolved').withArgs(0, true, true);
expect(await dao.balanceOf(account1)).to.equal(100);
await expect(dao.evaluateOutcome(1)).to.emit(dao, 'ValidationPoolResolved').withArgs(1, true);
await expect(dao.evaluateOutcome(1)).to.emit(dao, 'ValidationPoolResolved').withArgs(1, true, true);
expect(await dao.balanceOf(account1)).to.equal(200);
});
it('should not be able to evaluate outcome if quorum is not met', async () => {
time.increase(POOL_DURATION + 1);
await expect(dao.evaluateOutcome(0)).to.emit(dao, 'ValidationPoolResolved').withArgs(0, true);
await expect(dao.evaluateOutcome(0)).to.emit(dao, 'ValidationPoolResolved').withArgs(0, true, true);
const init = () => initiateValidationPool({ quorumNumerator: 1, quorumDenominator: 1 });
await expect(init()).to.emit(dao, 'ValidationPoolInitiated').withArgs(1);
expect(await dao.validationPoolCount()).to.equal(2);
time.increase(POOL_DURATION + 1);
await expect(dao.evaluateOutcome(1)).to.be.revertedWith('Quorum for this pool was not met');
await expect(dao.evaluateOutcome(1)).to.emit(dao, 'ValidationPoolResolved').withArgs(1, false, false);
});
describe('Validation pool options', () => {

View File

@ -243,7 +243,7 @@ describe('Work1', () => {
expect(pool.postIndex).to.equal(1);
expect(pool.stakeCount).to.equal(3);
await time.increase(86401);
await expect(dao.evaluateOutcome(1)).to.emit(dao, 'ValidationPoolResolved').withArgs(1, true);
await expect(dao.evaluateOutcome(1)).to.emit(dao, 'ValidationPoolResolved').withArgs(1, true, true);
});
it('should be able to submit work disapproval', async () => {