dgf-prototype/ethereum/contracts/DAO.sol

165 lines
6.1 KiB
Solidity

// 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
);
}
}