// SPDX-License-Identifier: Unlicense pragma solidity ^0.8.24; import {ERC721} from "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import "./ReputationHolder.sol"; struct Stake { bool inFavor; uint256 amount; address sender; uint256 tokenId; } struct ValidationPool { mapping(uint => Stake) stakes; uint stakeCount; address author; uint256 fee; uint duration; uint endTime; bool resolved; bool outcome; uint256 tokenIdFor; uint256 tokenIdAgainst; } 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 ERC721("Reputation", "REP"), ReputationHolder { mapping(uint256 tokenId => uint256) tokenValues; uint256 nextTokenId; uint256 public totalValue; 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, uint256 newTokenId); /// Inspect the value of a given reputation NFT function valueOf(uint256 tokenId) public view returns (uint256 value) { value = tokenValues[tokenId]; } /// Confirm ownership of a token and return its value. /// This should be used when receiving an NFT transfer, because otherwise /// someone could send any NFT with a tokenId matching one of ours. function verifiedValueOf( address owner, uint256 tokenId ) public view returns (uint256 value) { require(ownerOf(tokenId) == owner, "NFT owner mismatch"); value = valueOf(tokenId); } /// Internal function to mint a new reputation NFT function mint(uint256 value) internal returns (uint256 tokenId) { // Generate a new (sequential) ID for the token tokenId = nextTokenId++; // Mint the token, initially to be owned by the current contract. _mint(address(this), tokenId); tokenValues[tokenId] = value; // Keep track of total value minted // TODO: More sophisticated logic can compute total _available_, _active_ reputation totalValue += value; } /// Internal function to transfer value from one reputation token to another function transferValueFrom( uint256 fromTokenId, uint256 toTokenId, uint256 amount ) internal { require(amount >= 0, "Value transfer amount must be positive"); require( valueOf(fromTokenId) >= amount, "Source token has insufficient value" ); tokenValues[fromTokenId] -= amount; tokenValues[toTokenId] += amount; } /// Accept fee to initiate a validation pool /// TODO: Rather than accept author as a parameter, accept a reference to a forum post /// 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 pool.tokenIdFor = mint(msg.value / 2); pool.tokenIdAgainst = mint(msg.value / 2); stake(pool, address(this), true, pool.tokenIdFor); stake(pool, address(this), false, pool.tokenIdAgainst); emit ValidationPoolInitiated(poolIndex); } /// Internal function to register a stake for/against a validation pool function stake( ValidationPool storage pool, address sender, bool inFavor, uint256 tokenId ) internal { require(block.timestamp <= pool.endTime, "Pool end time has passed"); Stake storage _stake = pool.stakes[pool.stakeCount++]; _stake.sender = sender; _stake.inFavor = inFavor; _stake.amount = verifiedValueOf(sender, tokenId); _stake.tokenId = tokenId; } /// Accept reputation stakes toward a validation pool function onERC721Received( address, address from, uint256 tokenId, bytes calldata data ) public override returns (bytes4) { // `data` needs to encode the target validation pool, and the for/again boolean StakeData memory stakeParameters = abi.decode(data, (StakeData)); ValidationPool storage pool = validationPools[ stakeParameters.poolIndex ]; stake(pool, from, stakeParameters.inFavor, tokenId); return super.onERC721Received.selector; } /// 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 amountFor; uint256 amountAgainst; Stake storage _stake; for (uint i = 0; i < pool.stakeCount; i++) { _stake = pool.stakes[i]; if (_stake.inFavor) { amountFor += _stake.amount; } else { amountAgainst += _stake.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 = amountFor >= amountAgainst; pool.resolved = true; emit ValidationPoolResolved(votePasses, pool.tokenIdFor); // If the outcome is true, value of all stakes against the pool should be distributed among the stakes in favor. // If the outcome is false, value of all stakes for the pool should be distributed among the stakes against. uint256 amountFromWinners; uint256 amountFromLosers; // Collect the reputation from the losing stakes for (uint i = 0; i < pool.stakeCount; i++) { _stake = pool.stakes[i]; if (votePasses && !_stake.inFavor) { // Transfer value to the token that was minted in favor amountFromLosers += _stake.amount; transferValueFrom( _stake.tokenId, pool.tokenIdFor, _stake.amount ); } else if (!votePasses && _stake.inFavor) { // Transfer value to the token that was minted against amountFromLosers += _stake.amount; transferValueFrom( _stake.tokenId, pool.tokenIdAgainst, _stake.amount ); } else if ( votePasses && _stake.inFavor && _stake.tokenId != pool.tokenIdFor ) { // Tally the total value of winning stakes amountFromWinners += _stake.amount; } else if ( !votePasses && !_stake.inFavor && _stake.tokenId != pool.tokenIdAgainst ) { // Tally the total value of winning stakes amountFromWinners += _stake.amount; } } // Distribute reputation from losing stakes to winning stakes for (uint i = 0; i < pool.stakeCount; i++) { _stake = pool.stakes[i]; if ( votePasses && _stake.inFavor && _stake.tokenId != pool.tokenIdFor ) { uint256 reward = (amountFromLosers * _stake.amount) / amountFromWinners; transferValueFrom(pool.tokenIdAgainst, _stake.tokenId, reward); } else if ( !votePasses && !_stake.inFavor && _stake.tokenId != pool.tokenIdAgainst ) { uint256 reward = (amountFromLosers * _stake.amount) / amountFromWinners; transferValueFrom(pool.tokenIdFor, _stake.tokenId, reward); } } // Transfer minted reputation to the author // TODO: Handle multiple authors if (votePasses) { _transfer(address(this), pool.author, pool.tokenIdFor); } // Distribute fee proportionatly among all reputation holders for (uint tokenId = 0; tokenId < nextTokenId; tokenId++) { address recipient = ownerOf(tokenId); // Don't count tokens owned by this contract, as these are part of validation pools in progress if (recipient == address(this)) continue; uint256 share = (pool.fee * valueOf(tokenId)) / totalValue; // TODO: For efficiency this could be modified to hold the funds for recipients to withdraw payable(recipient).transfer(share); } } }