add lightweight bench contract
This commit is contained in:
parent
6b37cead66
commit
ef19b9bd66
|
@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue