// SPDX-License-Identifier: Unlicense pragma solidity ^0.8.24; import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; import "./IAcceptAvailability.sol"; struct Stake { bool inFavor; uint256 amount; address sender; } struct ValidationPool { mapping(uint => Stake) stakes; uint stakeCount; address author; 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; // 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(bool votePasses); /// Accept fee to initiate a validation pool /// TODO: Rather than accept author as a parameter, accept a reference to a forum post /// TODO: Handle multiple authors /// TODO: Constrain duration to allowable range function initiateValidationPool( address author, uint duration ) public payable returns (uint poolIndex) { require(msg.value > 0, "Fee is required to initiate validation pool"); poolIndex = validationPoolCount++; ValidationPool storage pool = validationPools[poolIndex]; pool.author = author; pool.fee = msg.value; pool.duration = duration; pool.endTime = block.timestamp + duration; // 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(address(this), msg.value); // TODO: We need a way to exclude this pending reputation from the total supply when computing fee distribution _stake(pool, author, msg.value / 2, true); _stake(pool, 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"); Stake storage s = pool.stakes[pool.stakeCount++]; s.sender = sender; s.inFavor = inFavor; s.amount = amount; } /// 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]; 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[pool.author]) { members[memberCount++] = pool.author; isMember[pool.author] = true; } pool.resolved = true; emit ValidationPoolResolved(votePasses); // Value of losing stakes should be distributed among winners, in proportion to their stakes uint256 amountFromWinners = votePasses ? stakedFor : stakedAgainst; uint256 amountFromLosers = votePasses ? stakedAgainst : stakedFor; 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); } } // 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 ); } }