// SPDX-License-Identifier: Unlicense pragma solidity ^0.8.24; import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; import "./IAcceptAvailability.sol"; import "hardhat/console.sol"; struct Post { uint id; address sender; address author; } struct Stake { uint id; bool inFavor; uint256 amount; address sender; } struct ValidationPool { uint id; uint postIndex; mapping(uint => Stake) stakes; uint stakeCount; uint256 fee; uint256 initialStakedFor; uint256 initialStakedAgainst; uint duration; uint endTime; bool resolved; bool outcome; } struct StakeData { uint poolIndex; bool inFavor; } /// This contract must manage validation pools and reputation, /// because otherwise there's no way to enforce appropriate permissions on /// transfer of value between reputation NFTs. contract DAO is ERC20("Reputation", "REP") { mapping(uint => address) public members; uint public memberCount; mapping(address => bool) public isMember; mapping(uint => ValidationPool) public validationPools; uint public validationPoolCount; mapping(uint => Post) public posts; uint public postCount; // ufixed8x1 constant mintingRatio = 1; // ufixed8x1 constant quorum = 0; // ufixed8x1 constant stakeForAuthor = 0.5; // ufixed8x1 constant winningRatio = 0.5; // TODO: Make parameters adjustable // TODO: Add forum parameters event ValidationPoolInitiated(uint poolIndex); event ValidationPoolResolved(uint poolIndex, bool votePasses); function addPost(address author) public returns (uint postIndex) { postIndex = postCount++; Post storage post = posts[postIndex]; post.author = author; post.sender = msg.sender; post.id = postIndex; } /// Accept fee to initiate a validation pool /// TODO: Handle multiple authors /// TODO: Constrain duration to allowable range function initiateValidationPool( uint postIndex, uint duration ) public payable returns (uint poolIndex) { require(msg.value > 0, "Fee is required to initiate validation pool"); Post storage post = posts[postIndex]; require(post.author != address(0), "Target post not found"); poolIndex = validationPoolCount++; ValidationPool storage pool = validationPools[poolIndex]; pool.postIndex = postIndex; pool.fee = msg.value; pool.duration = duration; pool.endTime = block.timestamp + duration; pool.id = poolIndex; // Because we need to stake part of the mited value for the pool an part against, // we mint two new tokens. // Here we assume a minting ratio of 1, and a stakeForAuthor ratio of 0.5 // Implementing this with adjustable parameters will require more advanced fixed point math. // TODO: Make minting ratio an adjustable parameter // TODO: Make stakeForAuthor an adjustable parameter _mint(post.author, msg.value); // TODO: We need a way to exclude this pending reputation from the total supply when computing fee distribution _stake(pool, post.author, msg.value / 2, true); _stake(pool, post.author, msg.value / 2, false); emit ValidationPoolInitiated(poolIndex); } /// Internal function to register a stake for/against a validation pool function _stake( ValidationPool storage pool, address sender, uint256 amount, bool inFavor ) internal { require(block.timestamp <= pool.endTime, "Pool end time has passed"); _transfer(sender, address(this), amount); uint stakeIndex = pool.stakeCount++; Stake storage s = pool.stakes[stakeIndex]; s.sender = sender; s.inFavor = inFavor; s.amount = amount; s.id = stakeIndex; } /// Accept reputation stakes toward a validation pool function stake(uint poolIndex, uint256 amount, bool inFavor) public { ValidationPool storage pool = validationPools[poolIndex]; _stake(pool, msg.sender, amount, inFavor); } /// Evaluate outcome of a validation pool function evaluateOutcome(uint poolIndex) public returns (bool votePasses) { ValidationPool storage pool = validationPools[poolIndex]; Post storage post = posts[pool.postIndex]; require( block.timestamp > pool.endTime, "Pool end time has not yet arrived" ); require(pool.resolved == false, "Pool is already resolved"); uint256 stakedFor; uint256 stakedAgainst; Stake storage s; for (uint i = 0; i < pool.stakeCount; i++) { s = pool.stakes[i]; if (s.inFavor) { stakedFor += s.amount; } else { stakedAgainst += s.amount; } } // Here we assume a quorum of 0 // TODO: Make quorum an adjustable parameter // 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. votePasses = stakedFor >= stakedAgainst; if (votePasses && !isMember[post.author]) { members[memberCount++] = post.author; isMember[post.author] = true; } pool.resolved = true; pool.outcome = votePasses; emit ValidationPoolResolved(poolIndex, votePasses); // Value of losing stakes should be di stributed among winners, in proportion to their stakes uint256 amountFromWinners = votePasses ? stakedFor : stakedAgainst; uint256 amountFromLosers = votePasses ? stakedAgainst : stakedFor; uint256 totalRewards; for (uint i = 0; i < pool.stakeCount; i++) { s = pool.stakes[i]; if (votePasses == s.inFavor) { uint256 reward = (amountFromLosers * s.amount) / amountFromWinners; _transfer(address(this), s.sender, s.amount + reward); totalRewards += reward; } } // Due to rounding, there may be some reward left over. Include this as a reward to the author. uint256 remainder = amountFromLosers - totalRewards; if (remainder > 0) { _transfer(address(this), post.author, remainder); } // Distribute fee proportionatly among all reputation holders for (uint i = 0; i < memberCount; i++) { address member = members[i]; uint256 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); } } /// Transfer REP to a contract, and call that contract's receiveTransfer method function stakeAvailability( address to, uint256 value, uint duration ) external returns (bool transferred) { transferred = super.transfer(to, value); if (transferred) IAcceptAvailability(to).acceptAvailability( msg.sender, value, duration ); } }