Compare commits

..

5 Commits

Author SHA1 Message Date
Ladd Hoffman 57b64f7977 Add reputation to post struct
Gitea Actions Demo / Explore-Gitea-Actions (push) Failing after 36s Details
2024-04-10 15:05:33 -05:00
Ladd Hoffman a40bbc8059 Pass author rewards via forum 2024-04-10 15:04:25 -05:00
Ladd Hoffman edd6d6d4d2 remove outdated comments 2024-04-10 14:57:04 -05:00
Ladd Hoffman 989bc9846e clarify logic in evaluateOutcome 2024-04-10 14:55:02 -05:00
Ladd Hoffman 77cd551602 Hold minted REP until outcome is evaluated 2024-04-10 14:21:42 -05:00
4 changed files with 73 additions and 55 deletions

View File

@ -1,15 +1,18 @@
// SPDX-License-Identifier: Unlicense
pragma solidity ^0.8.24;
import "./Reputation.sol";
struct Post {
uint id;
address sender;
address author;
string contentId;
uint reputation;
// TODO: citations
}
contract Forum {
contract Forum is Reputation {
mapping(uint => Post) public posts;
uint public postCount;
@ -27,4 +30,10 @@ contract Forum {
post.contentId = contentId;
emit PostAdded(postIndex);
}
function _onValidatePost(uint postIndex, uint amount) internal {
Post storage post = posts[postIndex];
post.reputation = amount;
_update(address(this), post.author, amount);
}
}

View File

@ -9,9 +9,8 @@ import "hardhat/console.sol";
struct ValidationPoolStake {
uint id;
bool inFavor;
uint256 amount;
uint amount;
address sender;
bool fromMint;
}
struct ValidationPoolParams {
@ -26,10 +25,11 @@ struct ValidationPool {
uint id;
uint postIndex;
address sender;
uint minted;
mapping(uint => ValidationPoolStake) stakes;
uint stakeCount;
ValidationPoolParams params;
uint256 fee;
uint fee;
uint endTime;
bool resolved;
bool outcome;
@ -63,18 +63,16 @@ contract ValidationPools is Reputation, Forum {
ValidationPool storage pool,
address sender,
uint256 amount,
bool inFavor,
bool fromMint
bool inFavor
) internal {
require(block.timestamp <= pool.endTime, "Pool end time has passed");
//_update(sender, address(this), amount);
// We don't call _update here; We defer that until evaluateOutcome.
uint stakeIndex = pool.stakeCount++;
ValidationPoolStake storage s = pool.stakes[stakeIndex];
s.sender = sender;
s.inFavor = inFavor;
s.amount = amount;
s.id = stakeIndex;
s.fromMint = fromMint;
}
/// Accept reputation stakes toward a validation pool
@ -84,7 +82,7 @@ contract ValidationPools is Reputation, Forum {
bool inFavor
) public {
ValidationPool storage pool = validationPools[poolIndex];
_stakeOnValidationPool(pool, msg.sender, amount, inFavor, false);
_stakeOnValidationPool(pool, msg.sender, amount, inFavor);
}
/// Accept reputation stakes toward a validation pool
@ -96,7 +94,7 @@ contract ValidationPools is Reputation, Forum {
) public {
ValidationPool storage pool = validationPools[poolIndex];
_spendAllowance(owner, msg.sender, amount);
_stakeOnValidationPool(pool, owner, amount, inFavor, false);
_stakeOnValidationPool(pool, owner, amount, inFavor);
}
/// Accept fee to initiate a validation pool
@ -140,11 +138,12 @@ contract ValidationPools is Reputation, Forum {
// 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(post.author, msg.value);
_mint(address(this), msg.value);
pool.minted = msg.value;
// Here we assume a stakeForAuthor ratio of 0.5
// TODO: Make stakeForAuthor an adjustable parameter
_stakeOnValidationPool(pool, post.author, msg.value / 2, true, true);
_stakeOnValidationPool(pool, post.author, msg.value / 2, false, true);
// _stakeOnValidationPool(pool, post.author, msg.value / 2, true, true);
// _stakeOnValidationPool(pool, post.author, msg.value / 2, false, true);
emit ValidationPoolInitiated(poolIndex);
}
@ -153,8 +152,8 @@ contract ValidationPools is Reputation, Forum {
ValidationPool storage pool = validationPools[poolIndex];
Post storage post = posts[pool.postIndex];
require(pool.resolved == false, "Pool is already resolved");
uint256 stakedFor;
uint256 stakedAgainst;
uint stakedFor;
uint stakedAgainst;
ValidationPoolStake storage s;
for (uint i = 0; i < pool.stakeCount; i++) {
s = pool.stakes[i];
@ -164,6 +163,8 @@ contract ValidationPools is Reputation, Forum {
stakedAgainst += s.amount;
}
}
stakedFor += pool.minted / 2;
stakedAgainst += pool.minted / 2;
// Special case for early evaluation if dao.totalSupply has been staked
require(
block.timestamp > pool.endTime ||
@ -207,24 +208,19 @@ contract ValidationPools is Reputation, Forum {
pool.resolved = true;
pool.outcome = 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;
// Only bindingPercent % should be redistributed
// Stake senders should get (100-bindingPercent) % back
// We have allowances for each stake. Time to collect from the losing stakes.
uint amountFromWinners = votePasses ? stakedFor : stakedAgainst;
uint totalRewards;
uint totalAllocated;
for (uint i = 0; i < pool.stakeCount; i++) {
s = pool.stakes[i];
uint bindingPercent = s.fromMint ? 100 : pool.params.bindingPercent;
bool redistributeLosingStakes = s.fromMint ||
pool.params.redistributeLosingStakes;
if (votePasses != s.inFavor) {
// Losing stake
// If this stake is from the minted fee, don't burn it
uint amount = (s.amount * bindingPercent) / 100;
if (redistributeLosingStakes) {
uint amount = (s.amount * pool.params.bindingPercent) / 100;
if (pool.params.redistributeLosingStakes) {
_update(s.sender, address(this), amount);
totalRewards += amount;
} else {
@ -232,21 +228,34 @@ contract ValidationPools is Reputation, Forum {
}
}
}
if (votePasses) {
// If vote passes, reward the author as though they had staked the winnin portion of the VP initial stake
totalRewards += pool.minted / 2;
uint reward = ((((totalRewards * pool.minted) / 2) /
amountFromWinners) * pool.params.bindingPercent) / 100;
totalAllocated += reward;
// Transfer REP to the forum instead of to the author directly
_onValidatePost(pool.postIndex, pool.minted / 2 + reward);
} else {
// If vote does not pass, divide the losing stake among the winners
totalRewards += pool.minted;
}
// Include the losign portion of the VP initial stake
// Issue rewards to the winners
for (uint i = 0; i < pool.stakeCount; i++) {
s = pool.stakes[i];
uint bindingPercent = s.fromMint ? 100 : pool.params.bindingPercent;
bool redistributeLosingStakes = s.fromMint ||
pool.params.redistributeLosingStakes;
if (redistributeLosingStakes && votePasses == s.inFavor) {
if (
pool.params.redistributeLosingStakes && votePasses == s.inFavor
) {
// Winning stake
// If this stake is from the minted fee, always redistribute it to the winners
uint reward = (((amountFromLosers * s.amount) /
amountFromWinners) * bindingPercent) / 100;
uint reward = (((totalRewards * s.amount) / amountFromWinners) *
pool.params.bindingPercent) / 100;
totalAllocated += reward;
_update(address(this), s.sender, reward);
}
}
// Due to rounding, some reward may be left over. Let's give it to the author.
uint remainder = totalRewards - totalAllocated;
if (remainder > 0) {
@ -256,7 +265,7 @@ contract ValidationPools is Reputation, Forum {
// Distribute fee proportionately among all reputation holders
for (uint i = 0; i < memberCount; i++) {
address member = members[i];
uint256 share = (pool.fee * balanceOf(member)) / totalSupply();
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);
}

View File

@ -73,7 +73,7 @@ describe('DAO', () => {
await expect(init()).to.emit(dao, 'ValidationPoolInitiated').withArgs(0);
expect(await dao.validationPoolCount()).to.equal(1);
expect(await dao.memberCount()).to.equal(0);
expect(await dao.balanceOf(account1)).to.equal(100);
expect(await dao.balanceOf(account1)).to.equal(0);
expect(await dao.totalSupply()).to.equal(POOL_FEE);
});
@ -131,18 +131,18 @@ describe('DAO', () => {
expect(await dao.balanceOf(account1)).to.equal(100);
expect(await dao.balanceOf(dao.target)).to.equal(0);
await initiateValidationPool();
expect(await dao.balanceOf(account1)).to.equal(200);
expect(await dao.balanceOf(dao.target)).to.equal(0);
expect(await dao.balanceOf(account1)).to.equal(100);
expect(await dao.balanceOf(dao.target)).to.equal(100);
});
it('should be able to stake before validation pool has elapsed', async () => {
await dao.stakeOnValidationPool(1, 10, true);
expect(await dao.balanceOf(account1)).to.equal(200);
expect(await dao.balanceOf(dao.target)).to.equal(0);
expect(await dao.balanceOf(account1)).to.equal(100);
expect(await dao.balanceOf(dao.target)).to.equal(100);
time.increase(POOL_DURATION + 1);
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);
expect(await dao.balanceOf(dao.target)).to.equal(0);
});
it('should not be able to stake after validation pool has elapsed', async () => {
@ -152,12 +152,12 @@ describe('DAO', () => {
it('should be able to stake against a validation pool', async () => {
await dao.stakeOnValidationPool(1, 10, false);
expect(await dao.balanceOf(account1)).to.equal(200);
expect(await dao.balanceOf(dao.target)).to.equal(0);
expect(await dao.balanceOf(account1)).to.equal(100);
expect(await dao.balanceOf(dao.target)).to.equal(100);
time.increase(POOL_DURATION + 1);
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);
expect(await dao.balanceOf(dao.target)).to.equal(0);
const pool = await dao.validationPools(1);
expect(pool.outcome).to.be.false;
});
@ -180,7 +180,7 @@ describe('DAO', () => {
});
it('should be able to evaluate outcome after duration has elapsed', async () => {
expect(await dao.balanceOf(dao.target)).to.equal(0);
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, true);
expect(await dao.memberCount()).to.equal(1);
@ -212,7 +212,7 @@ describe('DAO', () => {
expect(await dao.validationPoolCount()).to.equal(2);
time.increase(POOL_DURATION + 1);
await expect(dao.evaluateOutcome(0)).to.emit(dao, 'ValidationPoolResolved').withArgs(0, true, true);
expect(await dao.balanceOf(account1)).to.equal(200);
expect(await dao.balanceOf(account1)).to.equal(100);
await expect(dao.evaluateOutcome(1)).to.emit(dao, 'ValidationPoolResolved').withArgs(1, true, true);
expect(await dao.balanceOf(account1)).to.equal(200);
});
@ -244,9 +244,9 @@ describe('DAO', () => {
await expect(init()).to.emit(dao, 'ValidationPoolInitiated').withArgs(2);
await dao.connect(account1).stakeOnValidationPool(2, 10, true);
await dao.connect(account2).stakeOnValidationPool(2, 10, false);
expect(await dao.balanceOf(account1)).to.equal(200);
expect(await dao.balanceOf(account1)).to.equal(100);
expect(await dao.balanceOf(account2)).to.equal(100);
expect(await dao.balanceOf(dao.target)).to.equal(0);
expect(await dao.balanceOf(dao.target)).to.equal(100);
time.increase(POOL_DURATION + 1);
await dao.evaluateOutcome(2);
expect(await dao.balanceOf(account1)).to.equal(210);
@ -259,9 +259,9 @@ describe('DAO', () => {
await expect(init()).to.emit(dao, 'ValidationPoolInitiated').withArgs(2);
await dao.connect(account1).stakeOnValidationPool(2, 10, true);
await dao.connect(account2).stakeOnValidationPool(2, 10, false);
expect(await dao.balanceOf(account1)).to.equal(200);
expect(await dao.balanceOf(account1)).to.equal(100);
expect(await dao.balanceOf(account2)).to.equal(100);
expect(await dao.balanceOf(dao.target)).to.equal(0);
expect(await dao.balanceOf(dao.target)).to.equal(100);
time.increase(POOL_DURATION + 1);
await dao.evaluateOutcome(2);
expect(await dao.balanceOf(account1)).to.equal(200);
@ -274,9 +274,9 @@ describe('DAO', () => {
await expect(init()).to.emit(dao, 'ValidationPoolInitiated').withArgs(2);
await dao.connect(account1).stakeOnValidationPool(2, 10, true);
await dao.connect(account2).stakeOnValidationPool(2, 10, false);
expect(await dao.balanceOf(account1)).to.equal(200);
expect(await dao.balanceOf(account1)).to.equal(100);
expect(await dao.balanceOf(account2)).to.equal(100);
expect(await dao.balanceOf(dao.target)).to.equal(0);
expect(await dao.balanceOf(dao.target)).to.equal(100);
time.increase(POOL_DURATION + 1);
await dao.evaluateOutcome(2);
expect(await dao.balanceOf(account1)).to.equal(205);
@ -293,9 +293,9 @@ describe('DAO', () => {
await expect(init()).to.emit(dao, 'ValidationPoolInitiated').withArgs(2);
await dao.connect(account1).stakeOnValidationPool(2, 10, true);
await dao.connect(account2).stakeOnValidationPool(2, 10, false);
expect(await dao.balanceOf(account1)).to.equal(200);
expect(await dao.balanceOf(account1)).to.equal(100);
expect(await dao.balanceOf(account2)).to.equal(100);
expect(await dao.balanceOf(dao.target)).to.equal(0);
expect(await dao.balanceOf(dao.target)).to.equal(100);
time.increase(POOL_DURATION + 1);
await dao.evaluateOutcome(2);
expect(await dao.balanceOf(account1)).to.equal(200);
@ -312,9 +312,9 @@ describe('DAO', () => {
await expect(init()).to.emit(dao, 'ValidationPoolInitiated').withArgs(2);
await dao.connect(account1).stakeOnValidationPool(2, 10, true);
await dao.connect(account2).stakeOnValidationPool(2, 10, false);
expect(await dao.balanceOf(account1)).to.equal(200);
expect(await dao.balanceOf(account1)).to.equal(100);
expect(await dao.balanceOf(account2)).to.equal(100);
expect(await dao.balanceOf(dao.target)).to.equal(0);
expect(await dao.balanceOf(dao.target)).to.equal(100);
time.increase(POOL_DURATION + 1);
await dao.evaluateOutcome(2);
expect(await dao.balanceOf(account1)).to.equal(200);

View File

@ -210,7 +210,7 @@ describe('Work1', () => {
.to.emit(dao, 'ValidationPoolInitiated').withArgs(1)
.to.emit(work1, 'WorkApprovalSubmitted').withArgs(0, true);
expect(await dao.balanceOf(work1.target)).to.equal(0);
expect(await dao.balanceOf(account1)).to.equal(200);
expect(await dao.balanceOf(account1)).to.equal(100);
const post = await dao.posts(1);
expect(post.author).to.equal(account1);
expect(post.sender).to.equal(work1.target);
@ -219,7 +219,7 @@ describe('Work1', () => {
expect(pool.fee).to.equal(WORK1_PRICE);
expect(pool.sender).to.equal(work1.target);
expect(pool.postIndex).to.equal(1);
expect(pool.stakeCount).to.equal(3);
expect(pool.stakeCount).to.equal(1);
await time.increase(86401);
await expect(dao.evaluateOutcome(1)).to.emit(dao, 'ValidationPoolResolved').withArgs(1, true, true);
expect(await dao.balanceOf(account1)).to.equal(200);