244 lines
9.1 KiB
Solidity
244 lines
9.1 KiB
Solidity
// 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);
|
|
|
|
/// 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);
|
|
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);
|
|
require(valueOf(fromTokenId) >= amount);
|
|
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
|
|
function initiateValidationPool(
|
|
address author,
|
|
uint duration
|
|
) public payable returns (uint poolIndex) {
|
|
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);
|
|
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);
|
|
require(pool.resolved == false);
|
|
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;
|
|
// 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++) {
|
|
uint256 share = (pool.fee * valueOf(tokenId)) / totalValue;
|
|
address recipient = ownerOf(tokenId);
|
|
// TODO: For efficiency this could be modified to hold the funds for recipients to withdraw
|
|
payable(recipient).transfer(share);
|
|
}
|
|
}
|
|
}
|