add lightweight bench contract

This commit is contained in:
Ladd Hoffman 2024-06-28 10:46:04 -05:00
parent 6b37cead66
commit ef19b9bd66
1 changed files with 370 additions and 0 deletions

View File

@ -0,0 +1,370 @@
// SPDX-License-Identifier: Unlicense
pragma solidity ^0.8.24;
import "./DAO.sol";
struct LWVPoolParams {
uint duration;
uint[2] quorum; // [ Numerator, Denominator ]
uint[2] winRatio; // [ Numerator, Denominator ]
uint bindingPercent;
bool redistributeLosingStakes;
}
struct LWVPoolProps {
string postId;
uint fee;
uint minted;
uint endTime;
bool resolved;
bool outcome;
}
contract LightweightBench {
struct Transfer {
address from;
address to;
uint amount;
}
struct ProposedResult {
Transfer[] transfers;
uint stakedFor;
}
struct Stake {
uint id;
bool inFavor;
uint amount;
address sender;
string resultHash;
}
struct Pool {
uint id;
address sender;
mapping(string => ProposedResult) proposedResults;
string[] proposedResultHashes;
mapping(uint => Stake) stakes;
uint stakeCount;
LWVPoolParams params;
LWVPoolProps props;
bool callbackOnValidate;
bytes callbackData;
}
mapping(uint => Pool) public validationPools;
uint public validationPoolCount;
DAO dao;
uint constant minDuration = 1; // 1 second
uint constant maxDuration = 365_000_000 days; // 1 million years
uint[2] minQuorum = [1, 10];
function registerDAO(DAO dao_) external {
require(
address(dao) == address(0),
"A DAO has already been registered"
);
dao = dao_;
}
/// Accept fee to initiate a validation pool
function initiateValidationPool(
address sender,
string calldata postId,
uint duration,
uint[2] calldata quorum, // [Numerator, Denominator]
uint[2] calldata winRatio, // [Numerator, Denominator]
uint bindingPercent,
bool redistributeLosingStakes,
bool callbackOnValidate,
bytes calldata callbackData
) external payable returns (uint poolIndex) {
require(
msg.sender == address(dao),
"Only DAO contract may call initiateValidationPool"
);
require(duration >= minDuration, "Duration is too short");
require(duration <= maxDuration, "Duration is too long");
require(
minQuorum[1] * quorum[0] >= minQuorum[0] * quorum[1],
"Quorum is below minimum"
);
require(quorum[0] <= quorum[1], "Quorum is greater than one");
require(winRatio[0] <= winRatio[1], "Win ratio is greater than one");
require(bindingPercent <= 100, "Binding percent must be <= 100");
poolIndex = validationPoolCount++;
Pool storage pool = validationPools[poolIndex];
pool.id = poolIndex;
pool.sender = sender;
pool.props.postId = postId;
pool.props.fee = msg.value;
pool.props.endTime = block.timestamp + duration;
pool.params.quorum = quorum;
pool.params.winRatio = winRatio;
pool.params.bindingPercent = bindingPercent;
pool.params.redistributeLosingStakes = redistributeLosingStakes;
pool.params.duration = duration;
pool.callbackOnValidate = callbackOnValidate;
pool.callbackData = callbackData;
// We use our privilege as the DAO contract to mint reputation in proportion with the fee.
// Here we assume a minting ratio of 1
// TODO: Make minting ratio an adjustable parameter
dao.mint(address(dao), pool.props.fee);
pool.props.minted = msg.value;
dao.emitLWValidationPoolInitiated(poolIndex);
}
function proposeResult(
uint poolIndex,
string calldata resultHash,
Transfer[] calldata transfers
) external {
Pool storage pool = validationPools[poolIndex];
require(
block.timestamp <= pool.props.endTime,
"Pool end time has passed"
);
ProposedResult storage proposedResult = pool.proposedResults[
resultHash
];
pool.proposedResultHashes.push(resultHash);
require(
proposedResult.transfers.length == 0,
"This result hash has already been proposed"
);
for (uint i = 0; i < transfers.length; i++) {
proposedResult.transfers.push(transfers[i]);
}
}
/// Register a stake for/against a validation pool
function stakeOnValidationPool(
uint poolIndex,
string calldata resultHash,
address sender,
uint256 amount,
bool inFavor
) external {
require(
msg.sender == address(dao),
"Only DAO contract may call stakeOnValidationPool"
);
Pool storage pool = validationPools[poolIndex];
require(
block.timestamp <= pool.props.endTime,
"Pool end time has passed"
);
if (inFavor) {
ProposedResult storage proposedResult = pool.proposedResults[
resultHash
];
require(
proposedResult.transfers.length > 0,
"This result hash has not been proposed"
);
}
// We don't call _update here; We defer that until evaluateOutcome.
uint stakeIndex = pool.stakeCount++;
Stake storage s = pool.stakes[stakeIndex];
s.sender = sender;
s.inFavor = inFavor;
s.amount = amount;
s.id = stakeIndex;
s.resultHash = resultHash;
}
/// Evaluate outcome of a validation pool
function evaluateOutcome(uint poolIndex) public returns (bool votePasses) {
require(
msg.sender == address(dao),
"Only DAO contract may call evaluateOutcome"
);
Pool storage pool = validationPools[poolIndex];
require(pool.props.resolved == false, "Pool is already resolved");
uint stakedFor;
uint stakedAgainst;
Stake storage s;
for (uint i = 0; i < pool.stakeCount; i++) {
s = pool.stakes[i];
// Make sure the sender still has the required balance.
// If not, automatically decrease the staked amount.
if (dao.balanceOf(s.sender) < s.amount) {
s.amount = dao.balanceOf(s.sender);
}
if (s.inFavor) {
ProposedResult storage proposedResult = pool.proposedResults[
s.resultHash
];
proposedResult.stakedFor += s.amount;
} else {
stakedAgainst += s.amount;
}
}
// Determine the winning result hash
uint[] memory stakedForResult = new uint[](
pool.proposedResultHashes.length
);
uint winningResult;
for (uint i = 0; i < pool.proposedResultHashes.length; i++) {
string storage proposedResultHash = pool.proposedResultHashes[i];
ProposedResult storage proposedResult = pool.proposedResults[
proposedResultHash
];
stakedForResult[i] += proposedResult.stakedFor;
if (stakedForResult[i] > stakedForResult[winningResult]) {
winningResult = i;
}
}
// Only count stakes for the winning hash among the total staked in favor of the pool
for (uint i = 0; i < pool.stakeCount; i++) {
s = pool.stakes[i];
if (
s.inFavor &&
keccak256(bytes(s.resultHash)) ==
keccak256(bytes(pool.proposedResultHashes[winningResult]))
) {
stakedFor += s.amount;
}
}
stakedFor += pool.props.minted / 2;
stakedAgainst += pool.props.minted / 2;
if (pool.props.minted % 2 != 0) {
stakedFor += 1;
}
// Special case for early evaluation if dao.totalSupply has been staked
require(
block.timestamp > pool.props.endTime ||
stakedFor + stakedAgainst == dao.totalSupply(),
"Pool end time has not yet arrived"
);
// Check that quorum is met
if (
pool.params.quorum[1] * (stakedFor + stakedAgainst) <=
dao.totalSupply() * pool.params.quorum[0]
) {
// TODO: Refund fee
// TODO: this could be made available for the sender to withdraw
// payable(pool.sender).transfer(pool.props.fee);
pool.props.resolved = true;
dao.emitValidationPoolResolved(poolIndex, false, false);
// Callback if requested
if (pool.callbackOnValidate) {
dao.onValidate(
pool.sender,
votePasses,
false,
stakedFor,
stakedAgainst,
pool.callbackData
);
}
return false;
}
// 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 * pool.params.winRatio[1] >=
(stakedFor + stakedAgainst) * pool.params.winRatio[0];
pool.props.resolved = true;
pool.props.outcome = votePasses;
dao.emitValidationPoolResolved(poolIndex, votePasses, true);
// Value of losing stakes should be distributed among winners, in proportion to their stakes
// Only bindingPercent % should be redistributed
// Stake senders should get (1000000-bindingPercent) % back
uint amountFromWinners = votePasses ? stakedFor : stakedAgainst;
uint totalRewards;
uint totalAllocated;
for (uint i = 0; i < pool.stakeCount; i++) {
s = pool.stakes[i];
if (votePasses != s.inFavor) {
// Losing stake
uint amount = (s.amount * pool.params.bindingPercent) / 100;
if (pool.params.redistributeLosingStakes) {
dao.update(s.sender, address(dao), amount);
totalRewards += amount;
} else {
dao.burn(s.sender, amount);
}
}
}
if (votePasses) {
// If vote passes, reward the author as though they had staked the winning portion of the VP initial stake
// Here we assume a stakeForAuthor ratio of 0.5
// TODO: Make stakeForAuthor an adjustable parameter
totalRewards += pool.props.minted / 2;
// Include the losign portion of the VP initial stake
// Issue rewards to the winners
for (uint i = 0; i < pool.stakeCount; i++) {
s = pool.stakes[i];
if (
pool.params.redistributeLosingStakes &&
votePasses == s.inFavor
) {
// Winning stake
uint reward = (((totalRewards * s.amount) /
amountFromWinners) * pool.params.bindingPercent) / 100;
totalAllocated += reward;
dao.update(address(dao), s.sender, reward);
}
}
// Due to rounding, there may be some excess REP. Award it to the author.
uint remainder = totalRewards - totalAllocated;
if (pool.props.minted % 2 != 0) {
// We staked the odd remainder in favor of the post, on behalf of the author.
remainder += 1;
}
// Execute the transfers from the winning proposed result
ProposedResult storage result = pool.proposedResults[
pool.proposedResultHashes[winningResult]
];
for (uint i = 0; i < result.transfers.length; i++) {
dao.update(
result.transfers[i].from,
result.transfers[i].to,
result.transfers[i].amount
);
}
} else {
// If vote does not pass, divide the losing stake among the winners
totalRewards += pool.props.minted;
for (uint i = 0; i < pool.stakeCount; i++) {
s = pool.stakes[i];
if (
pool.params.redistributeLosingStakes &&
votePasses == s.inFavor
) {
// Winning stake
uint reward = (((totalRewards * s.amount) /
(amountFromWinners - pool.props.minted / 2)) *
pool.params.bindingPercent) / 100;
totalAllocated += reward;
dao.update(address(dao), s.sender, reward);
}
}
}
// Distribute fee proportionately among all reputation holders
dao.distributeFeeAmongMembers{value: pool.props.fee}();
// Callback if requested
if (pool.callbackOnValidate) {
dao.onValidate(
pool.sender,
votePasses,
true,
stakedFor,
stakedAgainst,
pool.callbackData
);
}
}
}