Skip to content
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

[WIP] Guilds Multivote offchain #299

Draft
wants to merge 2 commits into
base: v2.0
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
144 changes: 144 additions & 0 deletions contracts/erc20guild/BaseERC20Guild.sol
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import "@openzeppelin/contracts-upgradeable/utils/cryptography/ECDSAUpgradeable.
import "@openzeppelin/contracts-upgradeable/token/ERC20/IERC20Upgradeable.sol";
import "@openzeppelin/contracts-upgradeable/interfaces/IERC1271Upgradeable.sol";
import "@openzeppelin/contracts-upgradeable/utils/AddressUpgradeable.sol";
import "@openzeppelin/contracts/utils/cryptography/MerkleProof.sol";
import "../utils/PermissionRegistry.sol";
import "../utils/TokenVault.sol";

Expand Down Expand Up @@ -116,6 +117,11 @@ contract BaseERC20Guild {
uint256 timestamp;
}

struct VoteResult {
bool success;
string error;
}

mapping(address => TokenLock) public tokensLocked;

// All the signed votes that were executed, to avoid double signed vote execution.
Expand Down Expand Up @@ -609,4 +615,142 @@ contract BaseERC20Guild {
) public pure virtual returns (bytes32) {
return keccak256(abi.encodePacked(voter, proposalId, option, votingPower));
}

function validateMerkleTreeLeaf(
bytes32 root,
bytes32 voteHash,
bytes32[] memory proof
) public virtual returns (bool) {
return MerkleProof.verify(proof, root, voteHash);
}

/// @dev Execute signed vote
/// @param rootHash Hash of the merkle tree root
/// @param voter Address of voter
/// @param voteHash Hash of the vote
/// @param proof MerkleTree proof
/// @param proposalId ID of the proposal
/// @param option Option to vote on {proposalId}
/// @param votingPower Votingpower used to vote
/// @param signature Signed rootHash
function executeSignedVote(
bytes32 rootHash,
address voter,
bytes32 voteHash,
bytes32[] memory proof,
bytes32 proposalId,
uint256 option,
uint256 votingPower,
bytes memory signature
) public {
bytes32 hashedVote = hashVote(voter, proposalId, option, votingPower);
require(hashedVote == voteHash, "ERC20Guild: Invalid vote hash");
require(!signedVotes[hashedVote], "ERC20Guild: Already voted");
require(voter == rootHash.toEthSignedMessageHash().recover(signature), "ERC20Guild: Wrong signer");

// verify leaf
bool valid = validateMerkleTreeLeaf(rootHash, voteHash, proof);
require(valid, "ERC20Guild: Invalid merkle tree leaf");

// validate voting power and proposal
require(proposals[proposalId].endTime > block.timestamp, "ERC20Guild: Proposal ended, cannot be voted");
require(
(votingPowerOf(voter) >= votingPower) && (votingPower > proposalVotes[proposalId][voter].votingPower),
"ERC20Guild: Invalid votingPower amount"
);
require(
(proposalVotes[proposalId][voter].option == 0 && proposalVotes[proposalId][voter].votingPower == 0) ||
(proposalVotes[proposalId][voter].option == option &&
proposalVotes[proposalId][voter].votingPower < votingPower),
"ERC20Guild: Cannot change option voted, only increase votingPower"
);

signedVotes[hashedVote] = true;
_setVote(voter, proposalId, option, votingPower);
}

/// @dev Execute multiple signed votes
/// @param roots Array of merkle tree root hashes
/// @param voters Addresses of voters
/// @param votesHashes Hashes of the votes
/// @param proofs MerkleTree proofs array
/// @param proposalIds Proposal IDs to vote
/// @param options Array containing options to vote on
/// @param votingPowers VotingPower used on each vote
/// @param signatures Signed rootHashes
function executeSignedVotesBatches(
bytes32[] memory roots,
address[] memory voters,
bytes32[] memory votesHashes,
bytes32[][] memory proofs,
bytes32[] memory proposalIds,
uint256[] memory options,
uint256[] memory votingPowers,
bytes[] memory signatures
) public {
uint256 i = 0;
for (i = 0; i < roots.length; i++) {
executeSignedVote(
roots[i],
voters[i],
votesHashes[i],
proofs[i],
proposalIds[i],
options[i],
votingPowers[i],
signatures[i]
);
}
}

/// @dev Vote multiple proposals
/// @param proposalIds array of proposal ids to vote on
/// @param options array of options containing option to vote on
/// @param votingPowers array of votingPower per vote
function setMultipleVotes(
bytes32[] memory proposalIds,
uint256[] memory options,
uint256[] memory votingPowers
) public returns (VoteResult[] memory) {
require(
proposalIds.length == options.length && options.length == votingPowers.length,
"ERC20Guild: Invalid proposalIds, options or votingPowers length"
);
VoteResult[] memory result = new VoteResult[](proposalIds.length);

for (uint256 i = 0; i < proposalIds.length; i++) {
result[i] = _validateVote(proposalIds[i], options[i], votingPowers[i]);
require(result[i].success, result[i].error);
_setVote(msg.sender, proposalIds[i], options[i], votingPowers[i]);
}

return result;
}

function _validateVote(
bytes32 proposalId,
uint256 option,
uint256 votingPower
) private view returns (VoteResult memory) {
VoteResult memory result;
result.success = true;

if (proposals[proposalId].endTime < block.timestamp) {
result.success = false;
result.error = "ERC20Guild: Proposal ended, cannot be voted";
} else if (
votingPowerOf(msg.sender) < votingPower || votingPower < proposalVotes[proposalId][msg.sender].votingPower
) {
result.success = false;
result.error = "ERC20Guild: Invalid votingPower amount";
} else if (
proposalVotes[proposalId][msg.sender].votingPower > votingPower &&
(proposalVotes[proposalId][msg.sender].option != option &&
proposalVotes[proposalId][msg.sender].votingPower > votingPower)
) {
result.success = false;
result.error = "ERC20Guild: Cannot change option voted, only increase votingPower";
}
return result;
}
}
34 changes: 34 additions & 0 deletions contracts/erc20guild/IERC20Guild.sol
Original file line number Diff line number Diff line change
Expand Up @@ -182,4 +182,38 @@ interface IERC20Guild {
uint256 option,
uint256 votingPower
) external pure returns (bytes32);

function validateMerkleTreeLeaf(
bytes32 root,
bytes32 voteHash,
bytes32[] memory proof
) external returns (bool);

function executeSignedVote(
bytes32 rootHash,
address voter,
bytes32 voteHash,
bytes32[] memory proof,
bytes32 proposalId,
uint256 option,
uint256 votingPower,
bytes memory signature
) external;

function executeSignedVotesBatches(
bytes32[] memory roots,
address[] memory voters,
bytes32[] memory votesHashes,
bytes32[][] memory proofs,
bytes32[] memory proposalIds,
uint256[] memory options,
uint256[] memory votingPowers,
bytes[] memory signatures
) external;

function setMultipleVotes(
bytes32[] memory proposalIds,
uint256[] memory options,
uint256[] memory votingPowers
) external returns (bool[] memory);
}
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@
"hardhat-gas-reporter": "^1.0.4",
"husky": "^7.0.4",
"lint-staged": "^12.3.4",
"merkletreejs": "^0.3.9",
"pm2": "^2.9.3",
"promisify": "^0.0.3",
"pug": "^2.0.0-rc.4",
Expand Down
Loading