Skip to content

feat(voting): first implementation #12

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 25 commits into
base: dev
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
186 changes: 186 additions & 0 deletions contracts/voting/Ballot.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
// SPDX-License-Identifier: MIT

pragma solidity ^0.8.0;

import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol";
import "@openzeppelin/contracts/proxy/utils/Initializable.sol";
import "./VotingDelegation.sol";
import "./BallotFactory.sol";

/**
* @title Ballot
* @author Mathieu Bour, Valentin Pollart, Clarisse Tarrou and Charly Mancel for the DeepSquare Association.
* @dev Implementation of a ballot for a voting feature.
*/

contract Ballot is Ownable, Initializable {
// @dev Contract defining the DPS token
IERC20Metadata public DPS;

// @dev Contract allowing user to delegate their vote on specific topics
VotingDelegation public proxy;

// @dev The ballot factory
BallotFactory public factory;

struct Vote {
bytes32 choice;
bool hasVoted;
}

/**
* @notice The title or question of the vote
*/
bytes32 public title;

/**
* @notice The description of the vote
*/
bytes32 public description;

/**
* @notice Whether users can still vote or not
*/
bool public closed;

/**
* @notice The topic of the vote, used to know if user has delegated or has delegation on this vote
*/
bytes32 public topic;

/**
* @notice The different choices of the vote
*/
bytes32[] public choices;

/**
* @notice The results of this vote, which are available only once the vote has been closed
*/
mapping(bytes32 => uint256) public resultStorage;

/**
* @notice The minimum amount a user must have to be able to vote (25k DPS)
*/
uint256 public immutable votingLimit = 25e3 * 1e18;

/**
* @notice The list of all voters
*/
address[] internal voters;

/**
* @notice The choice selected by each voter
*/
mapping(address => Vote) internal votes;

/**
* @dev It is necessary to set these values in the ballot implementation since the balot factory use them to initialize clones
*/
constructor(IERC20Metadata _DPS, VotingDelegation _proxy) {
DPS = _DPS;
proxy = _proxy;
}

/**
* @notice Set up all state variable for clone contracts.
* @dev Can only be called once, usually right after contract creation.
* @param _DPS The contract defining the DPS token
* @param _proxy The contract allowing users to delegates their vote on specific topics
* @param _factory The factory that created this clone contract instance
* @param _title The subject or question of the vote
* @param _description The subject or question of the vote
* @param _topic The topic of the vote
* @param _choices The different choices for this vote
*/
function init(IERC20Metadata _DPS, VotingDelegation _proxy, string memory _title, string memory _description, string memory _topic, string[] memory _choices) public initializer {
title = keccak256(bytes(_title));
description = keccak256(bytes(_description));
topic = keccak256(bytes(_topic));
closed = false;
choices = new bytes32[](_choices.length);
for(uint i = 0; i < _choices.length; i++) {
choices[i] = keccak256(bytes(_choices[i]));
}
DPS = _DPS;
proxy = _proxy;
factory = BallotFactory(msg.sender);
}

/**
* @notice Returns all choices of the vote
*/
function getChoices() external view returns(bytes32[] memory) {
return choices;
}

function isValidChoice(bytes32 choiceHash) public view returns (bool) {
for (uint i = 0; i < choices.length; i++) {
if (choices[i] == choiceHash) {
return true;
}
}
return false;
}

/**
* @notice Send vote for given choice.
* @dev Requirements:
* - Vote MUST be open.
* - Choice MUST be a valid choice (belongs to the choices array).
* - Voter MUST NOT have delegated his vote.
* - Voter MUST have at least 25k DPS.
* @param choice The choice one want to vote for.
*/
function vote(string memory choice) external {
require(!closed, "Voting: Ballot is closed.");

require(!proxy.hasDelegated(msg.sender,topic), "Voting: Vote is delegated."); // Verify that voter has not granted proxy to somebody.

require(DPS.balanceOf(msg.sender) >= votingLimit, "Voting: Not enough DPS to vote."); // 25k DPS limit

bytes32 choiceHash = keccak256(bytes(choice));

require(isValidChoice(choiceHash), "Voting: Choice is invalid.");

if(!votes[msg.sender].hasVoted) {
votes[msg.sender].hasVoted = true;
voters.push(msg.sender);
}

votes[msg.sender].choice = choiceHash;
}

/**
* @notice Returns if given voter has voted
* @param voter The voter
*/
function hasVoted(address voter) external view returns(bool) {
return votes[voter].hasVoted;
}

/**
* @notice Returns the voter's vote
*/
function getVote(address voter) external view returns(bytes32) {
return votes[voter].choice;
}

/**
* @notice Close the vote, preventing users to vote afterwards
* @dev The caller MUST be the ballot factory owner
*/
function close() external {
require(msg.sender == factory.owner(), "Voting: Restricted to factory owner.");
require(!closed, "Voting: Ballot already closed.");

closed = true;

for(uint i = 0; i < voters.length; i++) { // if A has granted proxy to B
address voter = voters[i];
resultStorage[votes[voter].choice] += DPS.balanceOf(voter) + proxy.delegationAmount(voter, topic);
}

factory.archiveBallot();
}
}
99 changes: 99 additions & 0 deletions contracts/voting/BallotFactory.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
// SPDX-License-Identifier: MIT

pragma solidity ^0.8.0;

import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol";
import "@openzeppelin/contracts/proxy/Clones.sol";
import "@openzeppelin/contracts/access/AccessControl.sol";
import "./Ballot.sol";
import "./VotingDelegation.sol";

/**
* @title BallotFactory
* @author Mathieu Bour, Valentin Pollart, Clarisse Tarrou and Charly Mancel for the DeepSquare Association.
* @dev Implementation of ballot factory, accordingly to EIP 1167 nomenclature for minimal proxy implementation
*/
contract BallotFactory is Ownable, AccessControl {
// @dev Contract defining the DPS token
IERC20Metadata public DPS;

// @dev The address of the ballot implementation used as ballot contract proxy
address public implementationAddress;

// @dev The closable role, granted to ballot clones created by this factory.
bytes32 public constant CLOSABLE = keccak256("CLOSABLE");

/**
* @dev Event fired each time a ballot contract clone is created
* @param ballotAddress The address of the created clone contract
* @param title The title of the ballot
* @param description The URL of the file containing the full description of the ballot
* @param topic The topic of the ballot
* @param choices The choices of the ballot
*/
event BallotCreated(address ballotAddress, string title, string description, string topic, string[] choices);

/**
* @dev Event fired each time a ballot clone is closed
* @param ballotAddress The address of the closed clone ballot
*/
event BallotClosed(address ballotAddress);

/**
* @param _DPS The DPS contract address
* @param _implementationAddress The ballot contract proxy address
*/
constructor(IERC20Metadata _DPS, address _implementationAddress){
require(address(_DPS) != address(0), "BallotFactory: Implementation address should not be zero address");
require(_implementationAddress != address(0), "BallotFactory: Implementation address should not be zero address");
DPS = _DPS;
implementationAddress = _implementationAddress;
}

/**
* @notice Creates a new ballot contract clone
* @param title The title or question of the vote
* @param description The description of the vote
* @param topic The topic of the vote
* @param choices The different choices for this vote
*/
function createBallot(string memory title, string memory description, string memory topic, string[] memory choices) external onlyOwner {
Ballot implementation = Ballot(implementationAddress);
address cloneAddress = Clones.clone(implementationAddress);
Ballot(cloneAddress).init(implementation.DPS(), implementation.proxy(), title, description, topic, choices);
_grantRole(CLOSABLE, cloneAddress);
emit BallotCreated(cloneAddress, title, description, topic, choices);
}

/**
* @notice Archive a ballot and remove it from the active ballot list
* @dev It can be perform only by the ballot itself by calling this method within its close method.
*/
function archiveBallot() external onlyRole(CLOSABLE) {
emit BallotClosed(msg.sender);
}

/**
* @notice Set up the new ballot proxy address
* @param newAddress The address of the new ballot proxy contract
*/
function setImplementationAddress(address newAddress) external onlyOwner {
require(newAddress != address(0), "BallotFactory: Implementation address should not be zero address");
implementationAddress = newAddress;
}

/**
* @notice Returns all active ballot clone addresses.
*/
function getActiveBallots() external view returns (address[] memory) {
return activeBallotAddresses;
}

/**
* @notice Returns all archived ballot clone addresses.
*/
function getArchivedBallots() external view returns (address[] memory) {
return archivedBallotAddresses;
}
}
112 changes: 112 additions & 0 deletions contracts/voting/VotingDelegation.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
// SPDX-License-Identifier: MIT

pragma solidity ^0.8.0;

import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol";
import "./BallotFactory.sol";

/**
* @title BallotFactory
* @author Mathieu Bour, Valentin Pollart, Clarisse Tarrou and Charly Mancel for the DeepSquare Association.
* @dev Implementation of vote delegation contract for a voting feature
*/
contract VotingDelegation is Ownable {
// @dev Contract defining the DPS token
IERC20Metadata public DPS;

struct Grants {
mapping(address => uint256) indexes;
address[] delegators;
}

// @dev The minimum amount of DPS a user must have to receive vote delegations (25k DPS)
uint256 immutable delegatingLimit = 25e3 * 1e18;

// @dev A list of delegations received by a user user per topic
mapping(address => mapping(bytes32 => Grants)) internal delegates; // representative => tag => delegators

// @dev The representative of a user per topic
mapping(address => mapping(bytes32 => address)) internal proxyVoters; // delegator => tag => representative

/**
* @param _DPS The DPS contract address
*/
constructor(IERC20Metadata _DPS) {
require(address(_DPS) != address(0), "VotingDelegation: DPS address is zero.");

DPS = _DPS;
}

/**
* @notice Give delegation to another user on a topic. The representative MUST either have at least 25k DPS or be the zero address (which correspond to removing the delegation)
* @param to The user to give delegation to
* @param topic The topic on which you give delegation
*/
function delegate(address to, string memory topic) external {
require(DPS.balanceOf(to) >= delegatingLimit || to == address(0), "VotingDelegation: Proxy has not enough DPS.");
bytes32 topicHash = keccak256(bytes(topic));

if(proxyVoters[msg.sender][topicHash] != address(0)) {
Grants storage formerDelegateGrants = delegates[proxyVoters[msg.sender][topicHash]][topicHash];
uint256 senderIndex = formerDelegateGrants.indexes[msg.sender];
formerDelegateGrants.delegators[senderIndex] = formerDelegateGrants.delegators[formerDelegateGrants.delegators.length - 1];
formerDelegateGrants.delegators.pop();
formerDelegateGrants.indexes[msg.sender] = 0;
}

proxyVoters[msg.sender][topicHash] = to;

if(to != address(0)) {
Grants storage newDelegateGrants = delegates[to][topicHash];
newDelegateGrants.indexes[msg.sender] = newDelegateGrants.delegators.length;
newDelegateGrants.delegators.push(msg.sender);
}
}

/**
* @notice Computes the vote power a user have on a topic through delegation
* @dev The total does not include the user own vote power
* @param voter The address of the voter
* @param topic The delegation topic
*/
function delegationAmount(address voter, bytes32 topic) public view returns (uint256) {
uint256 total;
for(uint32 i = 0; i < delegates[voter][topic].delegators.length; i++) {
total += DPS.balanceOf(delegates[voter][topic].delegators[i]);
}
return total;
}

/**
* @notice Returns whether or not the voter has given delegation on given topic
* @param voter The address of the voter
* @param topic The delegation topic
*/
function hasDelegated(address voter, bytes32 topic) external view returns (bool) {
return proxyVoters[voter][topic] != address(0);
}

/**
* @notice Returns all delegations a voter has on a given topic
* @param to The address of the voter
* @param topic The delegation topic
*/
function delegators(address to, string memory topic) external view returns(address[] memory) {
bytes32 topicHash = keccak256(bytes(topic));
address[] memory proxies = new address[](delegates[to][topicHash].delegators.length);
for(uint32 i = 0; i < delegates[to][topicHash].delegators.length; i++) {
proxies[i] = delegates[to][topicHash].delegators[i];
}
return proxies;
}

/**
* @notice Returns the address of the representative of a voter has on a given topic
* @param from The address of the voter
* @param topic The delegation topic
*/
function representative(address from, string memory topic) external view returns(address) {
return proxyVoters[from][keccak256(bytes(topic))];
}
}
Loading