add proposal logic

This commit is contained in:
Ricardo Guilherme Schmidt
2023-11-24 17:01:45 -03:00
parent 38705662fa
commit 24e9c17613
8 changed files with 494 additions and 31 deletions

2
lib/forge-std vendored

2
lib/minime vendored

Submodule lib/minime updated: 6d9d4f5487...1f6820c245

View File

@@ -1,2 +1,3 @@
forge-std/=lib/forge-std/src/
@openzeppelin/contracts/=lib/openzeppelin-contracts/contracts
@vacp2p/minime/contracts/=lib/minime/contracts

View File

@@ -0,0 +1,77 @@
// SPDX-License-Identifier: CC0-1.0
pragma solidity >=0.8.19;
/**
* @notice Uses ethereum signed messages
*/
abstract contract MessageSigned {
/**
* @notice recovers address who signed the message
* @param _signHash operation ethereum signed message hash
* @param _messageSignature message `_signHash` signature
*/
function recoverAddress(
bytes32 _signHash,
bytes memory _messageSignature
)
internal
pure
returns(address)
{
uint8 v;
bytes32 r;
bytes32 s;
(v,r,s) = signatureSplit(_messageSignature);
return ecrecover(
_signHash,
v,
r,
s
);
}
/**
* @notice Hash a hash with `"\x19Ethereum Signed Message:\n32"`
* @param _hash Sign to hash.
* @return signHash Hash to be signed.
*/
function getSignHash(
bytes32 _hash
)
internal
pure
returns (bytes32 signHash)
{
signHash = keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", _hash));
}
/**
* @dev divides bytes signature into `uint8 v, bytes32 r, bytes32 s`
*/
function signatureSplit(bytes memory _signature)
internal
pure
returns (uint8 v, bytes32 r, bytes32 s)
{
require(_signature.length == 65, "Bad signature length");
// The signature format is a compact form of:
// {bytes32 r}{bytes32 s}{uint8 v}
// Compact means, uint8 is not padded to 32 bytes.
assembly {
r := mload(add(_signature, 32))
s := mload(add(_signature, 64))
// Here we are loading the last 32 bytes, including 31 bytes
// of 's'. There is no 'mload8' to do this.
//
// 'byte' is not working due to the Solidity parser, so lets
// use the second best option, 'and'
v := and(mload(add(_signature, 65)), 0xff)
}
if (v < 27) {
v += 27;
}
require(v == 27 || v == 28, "Bad signature version");
}
}

View File

@@ -1,6 +1,8 @@
// SPDX-License-Identifier: CC0-1.0
pragma solidity >=0.8.19;
import "../common/Controlled.sol";
/**
* @title Delegation
* @author Ricardo Guilherme Schmidt (Status Research & Development GmbH).

View File

@@ -0,0 +1,66 @@
// SPDX-License-Identifier: CC0-1.0
pragma solidity >=0.8.19;
import "./Delegation.sol";
abstract contract DelegationReader {
Delegation public delegation;
mapping(address => address) public delegationOf;
function validDelegate(
address _who
)
virtual
internal
view
returns(bool);
function precomputeDelegateOf(
address _who,
uint _block,
bool _revalidate
)
internal
{
delegationOf[_who] = _revalidate ? delegateOfAt(_who, _block) : cachedDelegateOfAt(_who, _block);
}
function delegateOfAt(
address _who,
uint _block
)
internal
view
returns(address delegate)
{
delegate = _who;
do {
delegate = delegation.delegatedToAt(delegate, _block);
} while (!validDelegate(delegate));
}
function cachedDelegateOfAt(
address _who,
uint _block
)
internal
view
returns(address delegate)
{
delegate = _who;
do {
address delegationOfd = delegationOf[delegate];
if(delegationOfd != address(0)){
return delegationOfd;
}else {
delegate = delegation.delegatedToAt(delegate, _block);
}
} while (!validDelegate(delegate));
}
}

View File

@@ -1,56 +1,373 @@
// SPDX-License-Identifier: CC0-1.0
pragma solidity >=0.8.19;
import { MiniMeToken } from "../token/MiniMeToken.sol";
import { MiniMeToken } from "@vacp2p/minime/contracts/MiniMeToken.sol";
import { MerkleProof } from "@openzeppelin/contracts/utils/cryptography/MerkleProof.sol";
import "../common/MessageSigned.sol";
import "./DelegationReader.sol";
contract Proposal {
enum Vote {
/**
* @title ProposalBase
* @author Ricardo Guilherme Schmidt (Status Research & Development GmbH)
* Store votes and tabulate results for Democracy. Cannot be used stand alone, only as base of Instance.
*/
contract Proposal is MessageSigned, DelegationReader, Controlled {
event VoteSignatures(uint256 position, bytes32 merkleTree);
event Voted(Vote indexed vote, address voter);
event PartialResult(Vote indexed vote, uint256 total);
event Claimed(Vote indexed vote, address claimer, address source);
event FinalResult(Vote result);
enum Vote {
Null,
Rejection,
Approval
Reject,
Approve
}
enum QuorumType {
Qualified, //60% of all influence
Absolute, //50% of all influence
Simple //simple present majority
}
MiniMeToken public immutable token;
address public immutable destination;
bytes public proposalData;
uint256 public immutable blockStart;
uint256 public immutable blockEnd;
MiniMeToken public token;
uint256 public tabulationBlockDelay;
bytes32 public dataHash;
uint public blockStart;
uint public voteBlockEnd;
QuorumType public quorum;
//votes storage
bytes32[] public signatures;
mapping(address voter => Vote vote) public voteMap;
//tabulation process
//tabulation process
uint256 public lastTabulationBlock;
mapping(address => address) public tabulated;
mapping(uint8 => uint256) public results;
mapping(address from => address to) public tabulated;
mapping(uint8 result => uint256 count) public results;
Vote public result;
modifier votingPeriod() {
modifier votingPeriod {
require(block.number >= blockStart, "Voting not started");
require(block.number <= blockEnd, "Voting ended");
require(block.number <= voteBlockEnd, "Voting ended");
_;
}
modifier tabulationPeriod() {
require(block.number > blockEnd, "Voting not ended");
modifier tabulationPeriod {
require(block.number > voteBlockEnd, "Voting not ended");
require(result == Vote.Null, "Tabulation ended");
_;
}
modifier tabulationFinished() {
modifier tabulationFinished {
require(lastTabulationBlock != 0, "Tabulation not started");
require(lastTabulationBlock + lastTabulationBlock < block.number, "Tabulation not ended");
require(lastTabulationBlock + tabulationBlockDelay < block.number, "Tabulation not ended");
_;
}
function vote(uint256 proposalId) external { }
function isApproved() public view returns (bool) { }
function finalize() external {
selfdestruct(msg.sender);
/**
* @notice constructs a "ProposalAbstract Library Contract" for Instance and ProposalFactory
*/
constructor(
MiniMeToken _token,
Delegation _delegation,
bytes32 _dataHash,
uint256 _tabulationBlockDelay,
uint256 _blockStart,
uint256 _blockEndDelay,
Proposal.QuorumType _quorum,
address payable _controller
)
Controlled(_controller)
{
require(address(token) == address(0), "Already initialized");
delegation = _delegation;
token = _token;
tabulationBlockDelay = _tabulationBlockDelay;
dataHash = _dataHash;
blockStart = _blockStart;
voteBlockEnd = blockStart + _blockEndDelay;
quorum = _quorum;
}
}
function validDelegate(
address _who
)
override
internal
view
returns(bool)
{
return voteMap[_who] != Vote.Null;
}
/**
* @notice include a merkle root of vote signatures
* @dev votes can be included as bytes32 hash(signature), in a merkle tree format,
* makes possible:
* - include multiple signatures by the same cost
* - voters don't have to pay anything to vote
* - the cost of ballot processing can be subsided to the party interested in the outcome
* @param _signatures merkle root of keccak256(address(this),uint8(vote))` leaf
*/
function voteSigned(bytes32 _signatures)
override
external
votingPeriod
{
emit VoteSignatures(
signatures.length,
_signatures
);
signatures.push(_signatures);
}
/**
* @notice include `msg.sender` vote
* @dev votes can be included by a direct call for contracts to vote directly
* contracts can also delegate to a externally owned account and submit by voteSigned method
* still important that contracts are able to vote directly is to allow a multisig to take a decision
* this is important because the safest delegates would be a Multisig
* @param _vote vote
*/
function voteDirect(Vote _vote)
override
external
votingPeriod
{
require(_vote != Vote.Null, "Bad _vote parameter");
voteMap[msg.sender] = _vote;
emit Voted(_vote, msg.sender);
}
/**
* @notice tabulates influence of a direct vote
* @param _voter address which called voteDirect
*/
function tabulateDirect(address _voter)
override
external
tabulationPeriod
{
Vote vote = voteMap[_voter];
require(vote != Vote.Null, "Not voted");
setTabulation(_voter, _voter, vote );
}
/**
* @notice tabulate influence of signed vote
* @param _vote vote used in signature
* @param _position position where the signature is sealed
* @param _proof merkle proof
* @param _signature plain signature used to produce the merkle leaf
*/
function tabulateSigned(Vote _vote, uint256 _position, bytes32[] calldata _proof, bytes calldata _signature)
override
external
tabulationPeriod
{
require(MerkleProof.verifyProof(_proof, signatures[_position], keccak256(_signature)), "Invalid proof");
address _voter = recoverAddress(getSignHash(voteHash(_vote)), _signature);
require(voteMap[_voter] == Vote.Null, "Already voted");
voteMap[_voter] = _vote;
emit Voted(_vote, _voter);
setTabulation(_voter, _voter, _vote);
}
/**
* @notice tabulates influence of non voter to his nearest delegate that voted
* @dev might run out of gas, to prevent this, precompute the delegation
* Should be called every time a nearer delegate tabulate their vote
* @param _source holder which not voted but have a delegate that voted
* @param _cached true if should use lookup values from precomputed
*/
function tabulateDelegated(address _source, bool _cached)
override
external
tabulationPeriod
{
address claimer = _cached ? cachedDelegateOfAt(_source, voteBlockEnd): delegateOfAt(_source, voteBlockEnd);
setTabulation(_source, claimer, voteMap[claimer]);
}
/**
* @notice precomputes a delegate vote based on current votes tabulated
* @dev to precompute a very long delegate chain, go from the end to start with _clean false.
* @param _start who will have delegate precomputed
* @param _revalidate if true dont use precomputed results
* TODO: fix long delegate chain recompute in case new votes
*/
function precomputeDelegation(
address _start,
bool _revalidate
)
override
external
tabulationPeriod
{
precomputeDelegateOf(_start, voteBlockEnd, _revalidate);
}
/**
* TODO:
* cannot have votes claimed for votes:
* accumulators(hold more than 1%) and burned SNT (held by TokenController address).
* when informed to contract, these accounts reduces totalSupply used for Qualified and Absolute quorums.
* if someone rich wants to use their influence,
* they will have to devide their balance in multiple addresses and delegate them to one address
* the objective is to make one rule for all on how to remove "out of circulation" in addresses like "Dev Reserve"
* this enhances the democracy, otherwise this locked accounts will end up influence of defaultDelegate
* function invalidate(address _accumulator) external;
*/
/**
* @notice finalizes and set result
*/
function finalize()
override
external
tabulationFinished
{
require(result == Vote.Null, "Already finalized");
Vote finalResult = calculateResult();
emit FinalResult(finalResult);
result = finalResult;
}
/**
* @notice wipes all from state
* @dev once the proposal result was read, it might be cleared up to free up state
*/
function clear()
override
external
onlyController
{
require(result != Vote.Null, "Not finalized");
selfdestruct(controller);
}
function isApproved() override external view returns (bool) {
require(result != Vote.Null, "Not finalized");
return result == Vote.Approve;
}
function isFinalized() override external view returns (bool) {
return result != Vote.Null;
}
function checkSignedVote(
Vote _vote,
uint256 _position,
bytes32[] calldata _proof,
bytes calldata _signature
)
external
view
returns (address)
{
require(MerkleProof.verifyProof(_proof, signatures[_position], keccak256(_signature)), "Invalid proof");
return recoverAddress(getSignHash(voteHash(_vote)), _signature);
}
function getVoteHash(Vote _vote) override external view returns (bytes32) {
return voteHash(_vote);
}
function getVotePrefixedHash(Vote _vote) external view returns (bytes32) {
return getSignHash(voteHash(_vote));
}
function delegateOf(address _who) external view returns(address) {
return delegateOfAt(_who, voteBlockEnd);
}
function cachedDelegateOf(address _who) external view returns(address) {
return cachedDelegateOfAt(_who, voteBlockEnd);
}
/**
* @notice get result
*/
function calculateResult()
public
view
returns(Vote finalResult)
{
uint256 approvals = results[uint8(Vote.Approve)];
bool approved;
if(quorum == QuorumType.Simple){
uint256 rejects = results[uint8(Vote.Reject)];
approved = approvals > rejects;
} else {
uint256 totalTokens = token.totalSupplyAt(voteBlockEnd);
if(quorum == QuorumType.Absolute) {
approved = approvals > (totalTokens / 2);
} else if(quorum == QuorumType.Qualified) {
approved = approvals > (totalTokens * 3) / 5;
}
}
finalResult = approved ? Vote.Approve : Vote.Reject;
}
function setTabulation(address _source, address _claimer, Vote _vote) internal {
require(_vote != Vote.Null, "Cannot be Vote.Null");
uint256 voterBalance = token.balanceOfAt(_source, voteBlockEnd);
if(voterBalance == 0) {
return;
}
address currentClaimer = tabulated[_source];
tabulated[_source] = _claimer;
emit Claimed(_vote, _claimer, _source);
if(currentClaimer != address(0))
{
require(currentClaimer != _source, "Voter already tabulated");
require(currentClaimer != _claimer, "Claimer already tabulated");
Vote oldVote = voteMap[currentClaimer];
if(oldVote == _vote) {
return;
}
emit PartialResult(oldVote, results[uint8(oldVote)] -= voterBalance);
}
emit PartialResult(_vote, results[uint8(_vote)] += voterBalance);
lastTabulationBlock = block.number;
}
function voteHash(Vote _vote) internal view returns (bytes32) {
require(_vote != Vote.Null, "Bad _vote parameter");
return keccak256(abi.encodePacked(address(this), _vote));
}
function findNearestDelegatable(address _source) internal view returns (address claimer, Vote vote){
vote = voteMap[_source];
require(vote == Vote.Null, "Not delegatable");
claimer = _source; // try finding first delegate from chain which voted
while(vote == Vote.Null) {
address claimerDelegate = delegation.delegatedToAt(claimer, voteBlockEnd);
claimer = claimerDelegate;
vote = voteMap[claimer]; //loads delegate vote.
}
}
function cachedFindNearestDelegatable(address _source) internal view returns (address claimer, Vote vote){
vote = voteMap[_source];
require(vote == Vote.Null, "Not delegatable");
claimer = _source; // try finding first delegate from chain which voted
while(vote == Vote.Null) {
address claimerDelegate = delegationOf[claimer];
if(claimerDelegate == address(0)){
claimerDelegate = delegation.delegatedToAt(claimer, voteBlockEnd);
}
claimer = claimerDelegate;
vote = voteMap[claimer]; //loads delegate vote.
}
}
}