From a7f3e5f2e454ff9d51d5905c37d728aeb2823192 Mon Sep 17 00:00:00 2001 From: Ladd Hoffman Date: Fri, 12 Apr 2024 16:54:48 -0500 Subject: [PATCH] Limit effects of negative citations --- ethereum/contracts/core/Forum.sol | 83 +++++++++++++++++-- ethereum/contracts/core/ValidationPools.sol | 13 +-- ethereum/test/Forum.js | 89 ++++++++++++++++++++- 3 files changed, 167 insertions(+), 18 deletions(-) diff --git a/ethereum/contracts/core/Forum.sol b/ethereum/contracts/core/Forum.sol index f285c53..8d1c178 100644 --- a/ethereum/contracts/core/Forum.sol +++ b/ethereum/contracts/core/Forum.sol @@ -21,9 +21,12 @@ struct Post { contract Forum is Reputation { mapping(uint => Post) public posts; uint public postCount; + mapping(uint => mapping(uint => int)) _edgeBalances; event PostAdded(uint postIndex); + // TODO: Add forum parameters + function addPost( address author, string calldata contentId, @@ -59,25 +62,89 @@ contract Forum is Reputation { } function _onValidatePost(uint postIndex, uint amount) internal { - _propagateValue(postIndex, int(amount)); + _propagateReputation(postIndex, int(amount), false, 0); } - function _propagateValue(uint postIndex, int amount) internal { - Post storage post = posts[postIndex]; + function _handleCitation( + uint postIndex, + Citation memory citation, + int amount, + bool initialNegative, + uint depth + ) internal returns (int outboundAmount) { + outboundAmount = (amount * citation.weightPercent) / 100; + int balanceToOutbound = _edgeBalances[postIndex][ + citation.targetPostIndex + ]; + if (initialNegative) { + if (outboundAmount < 0) { + outboundAmount = outboundAmount > -balanceToOutbound + ? outboundAmount + : -balanceToOutbound; + } else { + outboundAmount = outboundAmount < -balanceToOutbound + ? outboundAmount + : -balanceToOutbound; + } + } + int refund = _propagateReputation( + citation.targetPostIndex, + outboundAmount, + initialNegative || (depth == 0 && citation.weightPercent < 0), + depth + 1 + ); + outboundAmount -= refund; + _edgeBalances[postIndex][citation.targetPostIndex] += outboundAmount; + } + function _propagateReputation( + uint postIndex, + int amount, + bool initialNegative, + uint depth + ) internal returns (int refundToInbound) { + Post storage post = posts[postIndex]; int totalOutboundAmount; + // Propagate negative citations first for (uint i = 0; i < post.citations.length; i++) { - int share = (amount * post.citations[i].weightPercent) / 100; - totalOutboundAmount += share; - _propagateValue(post.citations[i].targetPostIndex, share); + if (post.citations[i].weightPercent < 0) { + int outboundAmount = _handleCitation( + postIndex, + post.citations[i], + amount, + initialNegative, + depth + ); + totalOutboundAmount += outboundAmount; + } + } + // Now propagate positive citations + for (uint i = 0; i < post.citations.length; i++) { + if (post.citations[i].weightPercent > 0) { + int outboundAmount = _handleCitation( + postIndex, + post.citations[i], + amount, + initialNegative, + depth + ); + totalOutboundAmount += outboundAmount; + } } int remaining = amount - totalOutboundAmount; if (remaining > 0) { _update(address(this), post.author, uint(remaining)); post.reputation += uint(remaining); } else { - _update(post.author, address(this), uint(-remaining)); - post.reputation -= uint(-remaining); + // Prevent reputation from being reduced below zero + if (int(post.reputation) + remaining >= 0) { + _update(post.author, address(this), uint(-remaining)); + post.reputation -= uint(-remaining); + } else { + refundToInbound = int(post.reputation) + remaining; + _update(post.author, address(this), post.reputation); + post.reputation = 0; + } } } } diff --git a/ethereum/contracts/core/ValidationPools.sol b/ethereum/contracts/core/ValidationPools.sol index be9ea07..0ac4c7d 100644 --- a/ethereum/contracts/core/ValidationPools.sol +++ b/ethereum/contracts/core/ValidationPools.sol @@ -56,8 +56,6 @@ contract ValidationPools is Reputation, Forum { bool quorumMet ); - // TODO: Add forum parameters - /// Internal function to register a stake for/against a validation pool function _stakeOnValidationPool( ValidationPool storage pool, @@ -98,7 +96,6 @@ contract ValidationPools is Reputation, Forum { } /// Accept fee to initiate a validation pool - /// TODO: Handle multiple authors function initiateValidationPool( uint postIndex, uint duration, @@ -140,10 +137,6 @@ contract ValidationPools is Reputation, Forum { // TODO: Make minting ratio an adjustable parameter _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); emit ValidationPoolInitiated(poolIndex); } @@ -176,7 +169,7 @@ contract ValidationPools is Reputation, Forum { pool.params.quorum[1] * (stakedFor + stakedAgainst) <= totalSupply() * pool.params.quorum[0] ) { - // Refund fee + // TODO: Refund fee // TODO: this could be made available for the sender to withdraw // payable(pool.sender).transfer(pool.fee); pool.resolved = true; @@ -230,7 +223,9 @@ 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 + // 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; uint reward = ((((totalRewards * pool.minted) / 2) / amountFromWinners) * pool.params.bindingPercent) / 100; diff --git a/ethereum/test/Forum.js b/ethereum/test/Forum.js index a81b06a..09c7917 100644 --- a/ethereum/test/Forum.js +++ b/ethereum/test/Forum.js @@ -30,7 +30,7 @@ describe('Forum', () => { } = {}) => dao.initiateValidationPool( postIndex ?? 0, duration ?? POOL_DURATION, - quorum ?? [1, 3], + quorum ?? [1, 10], winRatio ?? [1, 2], bindingPercent ?? 100, redistributeLosingStakes ?? true, @@ -114,5 +114,92 @@ describe('Forum', () => { expect(await dao.balanceOf(account1)).to.equal(0); expect(await dao.balanceOf(account2)).to.equal(100); }); + + it('should be able to reverse a negative citation with a negative citation', async () => { + await dao.addPost(account1, 'content-id', []); + await initiateValidationPool({ postIndex: 0 }); + await dao.evaluateOutcome(0); + expect(await dao.balanceOf(account1)).to.equal(100); + await dao.addPost(account2, 'second-content-id', [{ weightPercent: -100, targetPostIndex: 0 }]); + await initiateValidationPool({ postIndex: 1 }); + await time.increase(POOL_DURATION + 1); + await dao.evaluateOutcome(1); + expect(await dao.balanceOf(account1)).to.equal(0); + expect(await dao.balanceOf(account2)).to.equal(200); + await dao.addPost(account3, 'third-content-id', [{ weightPercent: -100, targetPostIndex: 1 }]); + await initiateValidationPool({ postIndex: 2 }); + await time.increase(POOL_DURATION + 1); + await dao.evaluateOutcome(2); + expect(await dao.balanceOf(account1)).to.equal(100); + expect(await dao.balanceOf(account2)).to.equal(0); + expect(await dao.balanceOf(account3)).to.equal(200); + }); + + it('forum reputation rewards are shared with validation pool policing rewards', async () => { + await dao.addPost(account1, 'content-id', []); + await initiateValidationPool({ postIndex: 0 }); + await dao.evaluateOutcome(0); + expect(await dao.balanceOf(account1)).to.equal(100); + await dao.addPost(account2, 'second-content-id', []); + await initiateValidationPool({ postIndex: 1 }); + await time.increase(POOL_DURATION + 1); + await dao.evaluateOutcome(1); + expect(await dao.balanceOf(account1)).to.equal(100); + expect(await dao.balanceOf(account2)).to.equal(100); + await dao.addPost(account3, 'third-content-id', [{ weightPercent: -100, targetPostIndex: 1 }]); + await initiateValidationPool({ postIndex: 2 }); + await dao.stakeOnValidationPool(2, 100, true); + await time.increase(POOL_DURATION + 1); + await dao.evaluateOutcome(2); + expect(await dao.balanceOf(account1)).to.equal(133); + expect(await dao.balanceOf(account2)).to.equal(34); + expect(await dao.balanceOf(account3)).to.equal(133); + }); + + it('should limit effects of negative references on prior positive references', async () => { + console.log('First post'); + await dao.addPost(account1, 'content-id', []); + await initiateValidationPool({ postIndex: 0 }); + await dao.evaluateOutcome(0); + expect(await dao.balanceOf(account1)).to.equal(100); + console.log('Second post'); + await dao.addPost(account2, 'second-content-id', [{ weightPercent: 50, targetPostIndex: 0 }]); + await initiateValidationPool({ postIndex: 1 }); + await time.increase(POOL_DURATION + 1); + await dao.evaluateOutcome(1); + expect(await dao.balanceOf(account1)).to.equal(150); + expect(await dao.balanceOf(account2)).to.equal(50); + console.log('Third post'); + await dao.addPost(account3, 'third-content-id', [{ weightPercent: -100, targetPostIndex: 1 }]); + await initiateValidationPool({ postIndex: 2, fee: 200 }); + await time.increase(POOL_DURATION + 1); + await dao.evaluateOutcome(2); + expect(await dao.balanceOf(account1)).to.equal(100); + expect(await dao.balanceOf(account2)).to.equal(0); + expect(await dao.balanceOf(account3)).to.equal(300); + }); + + it('should limit effects of negative references', async () => { + console.log('First post'); + await dao.addPost(account1, 'content-id', []); + await initiateValidationPool({ postIndex: 0 }); + await dao.evaluateOutcome(0); + expect(await dao.balanceOf(account1)).to.equal(100); + console.log('Second post'); + await dao.addPost(account2, 'second-content-id', [{ weightPercent: -100, targetPostIndex: 0 }]); + await initiateValidationPool({ postIndex: 1 }); + await time.increase(POOL_DURATION + 1); + await dao.evaluateOutcome(1); + expect(await dao.balanceOf(account1)).to.equal(0); + expect(await dao.balanceOf(account2)).to.equal(200); + console.log('Third post'); + await dao.addPost(account3, 'third-content-id', [{ weightPercent: -100, targetPostIndex: 1 }]); + await initiateValidationPool({ postIndex: 2, fee: 200 }); + await time.increase(POOL_DURATION + 1); + await dao.evaluateOutcome(2); + expect(await dao.balanceOf(account1)).to.equal(100); + expect(await dao.balanceOf(account2)).to.equal(0); + expect(await dao.balanceOf(account3)).to.equal(300); + }); }); });