dgf-prototype/ethereum/contracts/DAO.sol

301 lines
11 KiB
Solidity
Raw Normal View History

2024-03-04 19:33:06 -06:00
// SPDX-License-Identifier: Unlicense
pragma solidity ^0.8.24;
2024-03-10 12:57:30 -05:00
import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
2024-03-10 22:29:51 -05:00
import "./IAcceptAvailability.sol";
2024-03-17 21:00:31 -05:00
import "./IOnValidate.sol";
2024-03-04 19:33:06 -06:00
2024-03-11 13:39:56 -05:00
import "hardhat/console.sol";
2024-03-12 14:10:06 -05:00
struct Post {
2024-03-12 17:53:04 -05:00
uint id;
2024-03-12 14:10:06 -05:00
address sender;
address author;
2024-03-19 22:22:36 -05:00
string contentId;
2024-03-12 14:10:06 -05:00
}
2024-03-04 19:54:48 -06:00
struct Stake {
2024-03-12 17:53:04 -05:00
uint id;
2024-03-04 19:54:48 -06:00
bool inFavor;
uint256 amount;
2024-03-05 10:15:31 -06:00
address sender;
bool fromMint;
}
struct ValidationPoolParams {
uint quorumPPB;
uint bindingPercent;
bool redistributeLosingStakes;
2024-03-04 19:54:48 -06:00
}
struct ValidationPool {
2024-03-12 17:53:04 -05:00
uint id;
uint postIndex;
address sender;
2024-03-04 19:54:48 -06:00
mapping(uint => Stake) stakes;
uint stakeCount;
ValidationPoolParams params;
2024-03-05 13:30:36 -06:00
uint256 fee;
2024-03-05 10:15:31 -06:00
uint duration;
uint endTime;
2024-03-05 12:25:07 -06:00
bool resolved;
bool outcome;
2024-03-17 21:00:31 -05:00
bool callbackOnValidate;
bytes callbackData;
2024-03-05 10:15:31 -06:00
}
2024-03-04 19:33:06 -06:00
/// 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.
2024-03-10 12:57:30 -05:00
contract DAO is ERC20("Reputation", "REP") {
mapping(uint => address) public members;
uint public memberCount;
mapping(address => bool) public isMember;
2024-03-05 21:21:27 -06:00
mapping(uint => ValidationPool) public validationPools;
uint public validationPoolCount;
2024-03-12 14:10:06 -05:00
mapping(uint => Post) public posts;
uint public postCount;
2024-03-04 19:33:06 -06:00
// TODO: possible parameter for minting ratio
// TODO: possible parameter for stakeForAuthor
// TODO: possible parameter for winningRatio
2024-03-05 12:25:07 -06:00
// TODO: Add forum parameters
2024-03-26 15:20:54 -05:00
uint public constant minDuration = 1; // 1 second
uint public constant maxDuration = 365_000_000 days; // 1 million years
uint public constant minQuorumPPB = 333_333_333; // Parts per billion
2024-03-13 12:02:08 -05:00
2024-03-12 18:02:07 -05:00
event PostAdded(uint postIndex);
2024-03-05 12:25:07 -06:00
event ValidationPoolInitiated(uint poolIndex);
2024-03-27 14:03:57 -05:00
event ValidationPoolResolved(
uint poolIndex,
bool votePasses,
bool quorumMet
);
2024-03-04 19:54:48 -06:00
2024-03-19 22:22:36 -05:00
function addPost(
address author,
string calldata contentId
) external returns (uint postIndex) {
2024-03-12 14:10:06 -05:00
postIndex = postCount++;
Post storage post = posts[postIndex];
post.author = author;
post.sender = msg.sender;
2024-03-12 17:53:04 -05:00
post.id = postIndex;
2024-03-19 22:22:36 -05:00
post.contentId = contentId;
2024-03-12 18:02:07 -05:00
emit PostAdded(postIndex);
2024-03-12 14:10:06 -05:00
}
2024-03-04 19:54:48 -06:00
/// Accept fee to initiate a validation pool
2024-03-10 12:57:30 -05:00
/// TODO: Handle multiple authors
2024-03-05 13:30:36 -06:00
function initiateValidationPool(
2024-03-12 14:10:06 -05:00
uint postIndex,
2024-03-17 21:00:31 -05:00
uint duration,
2024-03-26 15:20:54 -05:00
uint quorumNumerator,
uint quorumDenominator,
uint bindingPercent,
bool redistributeLosingStakes,
2024-03-17 21:00:31 -05:00
bool callbackOnValidate,
bytes calldata callbackData
2024-03-19 22:22:36 -05:00
) external payable returns (uint poolIndex) {
2024-03-10 11:55:59 -05:00
require(msg.value > 0, "Fee is required to initiate validation pool");
2024-03-13 12:02:08 -05:00
require(duration >= minDuration, "Duration is too short");
require(duration <= maxDuration, "Duration is too long");
2024-03-26 15:20:54 -05:00
require(
(1_000_000_000 * quorumNumerator) / quorumDenominator >=
minQuorumPPB,
"Quorum is below minimum"
);
require(
quorumNumerator <= quorumDenominator,
"Quorum is greater than one"
);
require(bindingPercent <= 100, "Binding percent must be <= 100");
2024-03-12 17:53:04 -05:00
Post storage post = posts[postIndex];
require(post.author != address(0), "Target post not found");
2024-03-05 21:21:27 -06:00
poolIndex = validationPoolCount++;
2024-03-05 12:25:07 -06:00
ValidationPool storage pool = validationPools[poolIndex];
pool.sender = msg.sender;
2024-03-12 17:53:04 -05:00
pool.postIndex = postIndex;
2024-03-05 13:30:36 -06:00
pool.fee = msg.value;
pool.params.quorumPPB =
(1_000_000_000 * quorumNumerator) /
quorumDenominator;
pool.params.bindingPercent = bindingPercent;
pool.params.redistributeLosingStakes = redistributeLosingStakes;
2024-03-05 10:15:31 -06:00
pool.duration = duration;
pool.endTime = block.timestamp + duration;
2024-03-12 17:53:04 -05:00
pool.id = poolIndex;
2024-03-17 21:00:31 -05:00
pool.callbackOnValidate = callbackOnValidate;
pool.callbackData = callbackData;
2024-03-05 12:25:07 -06:00
// 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
2024-03-12 17:53:04 -05:00
_mint(post.author, msg.value);
2024-03-10 22:29:51 -05:00
// 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, true);
_stake(pool, post.author, msg.value / 2, false, true);
2024-03-05 12:25:07 -06:00
emit ValidationPoolInitiated(poolIndex);
}
/// Internal function to register a stake for/against a validation pool
2024-03-10 12:57:30 -05:00
function _stake(
2024-03-05 12:25:07 -06:00
ValidationPool storage pool,
address sender,
2024-03-10 12:57:30 -05:00
uint256 amount,
bool inFavor,
bool fromMint
2024-03-05 12:25:07 -06:00
) internal {
2024-03-07 21:27:37 -06:00
require(block.timestamp <= pool.endTime, "Pool end time has passed");
_update(sender, address(this), amount);
2024-03-12 17:53:04 -05:00
uint stakeIndex = pool.stakeCount++;
Stake storage s = pool.stakes[stakeIndex];
2024-03-10 12:57:30 -05:00
s.sender = sender;
s.inFavor = inFavor;
s.amount = amount;
2024-03-12 17:53:04 -05:00
s.id = stakeIndex;
s.fromMint = fromMint;
2024-03-05 10:15:31 -06:00
}
2024-03-04 19:54:48 -06:00
/// Accept reputation stakes toward a validation pool
2024-03-10 12:57:30 -05:00
function stake(uint poolIndex, uint256 amount, bool inFavor) public {
ValidationPool storage pool = validationPools[poolIndex];
_stake(pool, msg.sender, amount, inFavor, false);
2024-03-04 19:33:06 -06:00
}
2024-03-05 10:15:31 -06:00
/// Evaluate outcome of a validation pool
2024-03-05 13:30:36 -06:00
function evaluateOutcome(uint poolIndex) public returns (bool votePasses) {
2024-03-05 12:25:07 -06:00
ValidationPool storage pool = validationPools[poolIndex];
2024-03-12 17:53:04 -05:00
Post storage post = posts[pool.postIndex];
2024-03-07 21:27:37 -06:00
require(
block.timestamp > pool.endTime,
"Pool end time has not yet arrived"
);
require(pool.resolved == false, "Pool is already resolved");
2024-03-10 12:57:30 -05:00
uint256 stakedFor;
uint256 stakedAgainst;
Stake storage s;
2024-03-05 12:25:07 -06:00
for (uint i = 0; i < pool.stakeCount; i++) {
2024-03-10 12:57:30 -05:00
s = pool.stakes[i];
if (s.inFavor) {
stakedFor += s.amount;
2024-03-05 12:25:07 -06:00
} else {
2024-03-10 12:57:30 -05:00
stakedAgainst += s.amount;
2024-03-05 12:25:07 -06:00
}
}
2024-03-26 15:20:54 -05:00
// Check that quorum is met
2024-03-27 14:03:57 -05:00
if (
1_000_000_000 * (stakedFor + stakedAgainst) <=
totalSupply() * pool.params.quorumPPB
) {
// TODO: refund stakes
// Callback if requested
if (pool.callbackOnValidate) {
IOnValidate(pool.sender).onValidate(
votePasses,
false,
pool.callbackData
);
}
emit ValidationPoolResolved(poolIndex, false, false);
return false;
}
2024-03-05 12:25:07 -06:00
// 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.
2024-03-10 12:57:30 -05:00
votePasses = stakedFor >= stakedAgainst;
2024-03-12 17:53:04 -05:00
if (votePasses && !isMember[post.author]) {
members[memberCount++] = post.author;
isMember[post.author] = true;
2024-03-05 13:30:36 -06:00
}
2024-03-10 12:57:30 -05:00
pool.resolved = true;
2024-03-12 17:53:04 -05:00
pool.outcome = votePasses;
2024-03-27 14:03:57 -05:00
emit ValidationPoolResolved(poolIndex, votePasses, true);
// Value of losing stakes should be distributed among winners, in proportion to their stakes
2024-03-10 12:57:30 -05:00
uint256 amountFromWinners = votePasses ? stakedFor : stakedAgainst;
uint256 amountFromLosers = votePasses ? stakedAgainst : stakedFor;
// Only bindingPercent % should be redistributed
// Stake senders should get (100-bindingPercent) % back
uint256 totalAllocated;
2024-03-05 13:30:36 -06:00
for (uint i = 0; i < pool.stakeCount; i++) {
2024-03-10 12:57:30 -05:00
s = pool.stakes[i];
bool redistributeLosingStakes = s.fromMint ||
pool.params.redistributeLosingStakes;
uint bindingPercent = s.fromMint ? 100 : pool.params.bindingPercent;
2024-03-10 12:57:30 -05:00
if (votePasses == s.inFavor) {
// Winning stake
// If this stake is from the minted fee, always redistribute it to the winners
uint reward = redistributeLosingStakes
? ((s.amount * amountFromLosers) / amountFromWinners) *
(bindingPercent / 100)
: 0;
_update(address(this), s.sender, s.amount + reward);
totalAllocated += reward;
} else {
// Losing stake
uint refund = (s.amount * (100 - bindingPercent)) / 100;
if (refund > 0) {
_update(address(this), s.sender, refund);
}
// If this stake is from the minted fee, don't burn it
if (!redistributeLosingStakes) {
uint amountToBurn = (s.amount *
pool.params.bindingPercent) / 100;
_burn(address(this), amountToBurn);
totalAllocated += amountToBurn;
}
totalAllocated += refund;
2024-03-05 13:30:36 -06:00
}
}
// Due to rounding, there may be some REP left over. Include this as a reward to the author.
uint256 remainder = amountFromLosers - totalAllocated;
2024-03-11 13:39:56 -05:00
if (remainder > 0) {
_update(address(this), post.author, remainder);
2024-03-11 13:39:56 -05:00
}
2024-03-05 13:30:36 -06:00
// Distribute fee proportionatly among all reputation holders
2024-03-10 12:57:30 -05:00
for (uint i = 0; i < memberCount; i++) {
address member = members[i];
uint256 share = (pool.fee * balanceOf(member)) / totalSupply();
2024-03-05 21:21:27 -06:00
// TODO: For efficiency this could be modified to hold the funds for recipients to withdraw
2024-03-10 12:57:30 -05:00
payable(member).transfer(share);
2024-03-05 13:30:36 -06:00
}
2024-03-17 21:00:31 -05:00
// Callback if requested
if (pool.callbackOnValidate) {
2024-03-27 14:03:57 -05:00
IOnValidate(pool.sender).onValidate(
votePasses,
true,
pool.callbackData
);
2024-03-17 21:00:31 -05:00
}
2024-03-05 12:25:07 -06:00
}
2024-03-10 22:29:51 -05:00
/// 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
);
}
2024-03-04 19:33:06 -06:00
}
/// Convenience contract to extend for other contracts that will be initialized to
/// interact with a DAO contract.
contract DAOContract {
DAO immutable dao;
constructor(DAO dao_) {
dao = dao_;
}
}