Add polling contract (#35)

This commit is contained in:
Szymon Szlachtowicz 2021-09-01 01:06:04 +02:00 committed by GitHub
parent aae698c9b7
commit 0237513e5a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 2290 additions and 350 deletions

View File

@ -0,0 +1,5 @@
{
"extends": [
"../../.eslintrc.json"
]
}

View File

@ -0,0 +1,10 @@
{
"extends": "solhint:recommended",
"plugins": ["prettier"],
"rules": {
"prettier/prettier": "error",
"compiler-version": ["error", "^0.8.5"],
"quotes": ["error","single"],
"func-visibility": ["warn",{"ignoreConstructors":true}]
}
}

View File

@ -0,0 +1,186 @@
# status-waku-voting/contracts
Status white label proposal contract
## Voting Contract
Voting contract is a smart contract created for purpose of
having smart contract that can create and save results of proposals.
This Contract is responsible for creating voting rooms in which you can vote for or against them.
Lifecycle of voting room:
1. initialize voting room.
2. period of time when votes are accepted.
3. voting time is finished votes are no longer accepted.
### Types
Main types used for voting are:
- `VotingRoom`
Is a type that hold information about voting room for a given proposal
#### Fields
```solidity
//block at which room was created
uint256 startBlock;
//timestamp after which new votes won't be accepted
uint256 endAt;
// question of a proposal which voting room describes
string question;
// description for proposal
string description;
// amount of summed votes for
uint256 totalVotesFor;
// amount of summed votes against
uint256 totalVotesAgainst;
//list of addresses that already voted
address[] voters;
```
- `Vote`
Is a type that hold information about vote for a given voting room
#### Fields
```solidity
//address of a voter
address voter;
// encoded roomId and type
// first bit this field is a vote type:
// 1 is a vote for
// 0 is a vote against
// rest of this field is a roomId shifted one bit to
// the left
uint256 roomIdAndType;
// amount of token used to vote
uint256 tokenAmount;
//signature of vote
bytes32 r;
bytes32 vs;
```
### Constants and variables
- `token`
Variable that holds address of token used for vote verification. It is assigned at contract creation.
- `VOTING_LENGTH`
Constant describing length of voting room in seconds
TODO:
- maybe set voting length per voting room ?
- `EIP712DOMAIN_TYPEHASH`
Constant holding type hash of EIP712 domain as per EIP712 specification
- `VOTE_TYPEHASH`
Constant holding type hash of Vote as per EIP712 specification
- `DOMAIN_SEPARATOR`
Variable holding hash of domain separator according to EIP712 spec. Assigned at smart contract creation.
- `voted`
this variable holds information if given address voted in a given voting room. So it is a mapping of room id to mapping of addresses to bools which say whether or not given address voted.
- `votingRooms`
Array that holds all voting rooms. roomId of voting room is equivalent to its index in array
### Signing with EIP712
This smart contract uses EIP712 for signing vote msg.
The structure of typed data for vote messages is as follows:
```ts
{
types: {
EIP712Domain: [
{ name: 'name', type: 'string' },
{ name: 'version', type: 'string' },
{ name: 'chainId', type: 'uint256' },
{ name: 'verifyingContract', type: 'address' },
],
Vote: [
{ name: 'roomIdAndType', type: 'uint256' },
{ name: 'tokenAmount', type: 'uint256' },
{ name: 'voter', type: 'address' },
],
},
primaryType: 'Vote',
domain: {
name: 'Voting Contract',
version: '1',
chainId: chainId,
verifyingContract: contract.address,
},
message: {
voter: voterAddress,
tokenAmount: tokenAmount,
roomIdAndType: roomIdAndType
}
}
```
For more information about EIP-712 go to [docs](https://eips.ethereum.org/EIPS/eip-712)
### Functions
- `constructor(IERC20 _address)`
assigns `_address` to `token` and generates `DOMAIN_SEPARATOR`
- `getVotingRooms()`
returns votingRooms
- `listRoomVoters(uint256 roomId)`
returns a list of voters for a given voting room. Reverts if roomId doesn't exist.
- `initializeVotingRoom(string calldata question,string calldata description,uint256 voteAmount)`
Creates a new voting room with vote for set to voteAmount.
First checks if voter has enough tokens to set vote for.
Then creates a new voting room.
`startBlock` is set as current block number.
`endAt` is set a current block timestamp plus.`VOTING_LENGTH`.
`question` is set as argument `question`.
`description` is set as argument `description`.
`totalVotesFor` is set as argument `voteAmount`.
Mapping `voted` of new voting room id of `msg.sender` is set to true to reflect that message sender has voted on this voting room with `voteAmount`.
`votingRooms` are appended with newVotingRoom and `voters` in this new appended element are appended with message sender.
After room init `VotingRoomStarted` is emitted.
- `verify(Vote calldata vote,bytes32 r,bytes32 vs)`
Function used to verify that `vote` was signed by `vote.voter` as per EIP712 specification.
See [docs](https://eips.ethereum.org/EIPS/eip-712) for more info.
- `updateRoomVotes(Vote calldata vote, uint256 roomId)`
Sets totalVotes amount of voting room with index corresponding to `roomId`.
If voting first bit of `vote.roomIdAndType` is 1 that means that vote is for and `vote.tokenAmount` is added to `votingRooms[roomId].totalVotesFor`, otherwise if `vote.roomIdAndType` is 0 `vote.tokenAmount` is added to `votingRooms[roomId].totalVotesAgainst`.
After that add new address to room `voters` and updates mapping `voted` accordingly.
- `castVotes(Vote[] calldata votes)`
Function used to cast votes in rooms.
Function accepts an array of votes of type `Vote`.
All votes are looped through and verified that votes are:
- properly signed
- voter has enough tokens to vote
- voting room exists
- voting room hasn't been closed
Vote verification is as follows.
First roomId is decoded from `vote.roomIdAndType` which means shifting it to the right once.
Then it is verified that voting room with given roomId exists and isn't closed if not whole function reverts, this is to discourage grouping votes for different voting rooms together (! maybe it should be changed so that votes for multiple voting rooms can be cast ? !).
After that it is verified that `vote` has been signed by `vote.voter`. If not function goes to another vote in array (IDEA: maybe vote verification failed should be emitted ?).
Then it is checked that `vote.voter` didn't vote in this vote room before if he did function goes to another voter (IDEA: emit alreadyVoted ?).
Last check is whether `vote.voter` has enough tokens to vote. If he does not `NotEnoughToken` is emitted and function goes to another voter. If he does voting room is updated with `updateRoomVotes` and `VoteCast` is emitted.
TODO:
- not emit on Not enough tokens ?
- emit on wrong signature ?
- if instead of require for voting room not found ?
- if instead of require for vote closed ?

View File

@ -0,0 +1,230 @@
{
"contractName": "ERC20",
"abi": [
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"name": "from",
"type": "address"
},
{
"indexed": true,
"name": "to",
"type": "address"
},
{
"indexed": false,
"name": "value",
"type": "uint256"
}
],
"name": "Transfer",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"name": "owner",
"type": "address"
},
{
"indexed": true,
"name": "spender",
"type": "address"
},
{
"indexed": false,
"name": "value",
"type": "uint256"
}
],
"name": "Approval",
"type": "event"
},
{
"constant": true,
"inputs": [],
"name": "totalSupply",
"outputs": [
{
"name": "",
"type": "uint256"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": true,
"inputs": [
{
"name": "owner",
"type": "address"
}
],
"name": "balanceOf",
"outputs": [
{
"name": "",
"type": "uint256"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": true,
"inputs": [
{
"name": "owner",
"type": "address"
},
{
"name": "spender",
"type": "address"
}
],
"name": "allowance",
"outputs": [
{
"name": "",
"type": "uint256"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": false,
"inputs": [
{
"name": "to",
"type": "address"
},
{
"name": "value",
"type": "uint256"
}
],
"name": "transfer",
"outputs": [
{
"name": "",
"type": "bool"
}
],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
},
{
"constant": false,
"inputs": [
{
"name": "spender",
"type": "address"
},
{
"name": "value",
"type": "uint256"
}
],
"name": "approve",
"outputs": [
{
"name": "",
"type": "bool"
}
],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
},
{
"constant": false,
"inputs": [
{
"name": "from",
"type": "address"
},
{
"name": "to",
"type": "address"
},
{
"name": "value",
"type": "uint256"
}
],
"name": "transferFrom",
"outputs": [
{
"name": "",
"type": "bool"
}
],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
},
{
"constant": false,
"inputs": [
{
"name": "spender",
"type": "address"
},
{
"name": "addedValue",
"type": "uint256"
}
],
"name": "increaseAllowance",
"outputs": [
{
"name": "",
"type": "bool"
}
],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
},
{
"constant": false,
"inputs": [
{
"name": "spender",
"type": "address"
},
{
"name": "subtractedValue",
"type": "uint256"
}
],
"name": "decreaseAllowance",
"outputs": [
{
"name": "",
"type": "bool"
}
],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
}
],
"bytecode": "0x608060405234801561001057600080fd5b506105dd806100206000396000f3fe608060405234801561001057600080fd5b50600436106100a5576000357c01000000000000000000000000000000000000000000000000000000009004806370a082311161007857806370a0823114610166578063a457c2d71461018c578063a9059cbb146101b8578063dd62ed3e146101e4576100a5565b8063095ea7b3146100aa57806318160ddd146100ea57806323b872dd14610104578063395093511461013a575b600080fd5b6100d6600480360360408110156100c057600080fd5b50600160a060020a038135169060200135610212565b604080519115158252519081900360200190f35b6100f2610290565b60408051918252519081900360200190f35b6100d66004803603606081101561011a57600080fd5b50600160a060020a03813581169160208101359091169060400135610296565b6100d66004803603604081101561015057600080fd5b50600160a060020a03813516906020013561035f565b6100f26004803603602081101561017c57600080fd5b5035600160a060020a031661040f565b6100d6600480360360408110156101a257600080fd5b50600160a060020a03813516906020013561042a565b6100d6600480360360408110156101ce57600080fd5b50600160a060020a038135169060200135610475565b6100f2600480360360408110156101fa57600080fd5b50600160a060020a038135811691602001351661048b565b6000600160a060020a038316151561022957600080fd5b336000818152600160209081526040808320600160a060020a03881680855290835292819020869055805186815290519293927f8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925929181900390910190a350600192915050565b60025490565b600160a060020a03831660009081526001602090815260408083203384529091528120546102ca908363ffffffff6104b616565b600160a060020a03851660009081526001602090815260408083203384529091529020556102f98484846104cb565b600160a060020a0384166000818152600160209081526040808320338085529083529281902054815190815290519293927f8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925929181900390910190a35060019392505050565b6000600160a060020a038316151561037657600080fd5b336000908152600160209081526040808320600160a060020a03871684529091529020546103aa908363ffffffff61059816565b336000818152600160209081526040808320600160a060020a0389168085529083529281902085905580519485525191937f8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925929081900390910190a350600192915050565b600160a060020a031660009081526020819052604090205490565b6000600160a060020a038316151561044157600080fd5b336000908152600160209081526040808320600160a060020a03871684529091529020546103aa908363ffffffff6104b616565b60006104823384846104cb565b50600192915050565b600160a060020a03918216600090815260016020908152604080832093909416825291909152205490565b6000828211156104c557600080fd5b50900390565b600160a060020a03821615156104e057600080fd5b600160a060020a038316600090815260208190526040902054610509908263ffffffff6104b616565b600160a060020a03808516600090815260208190526040808220939093559084168152205461053e908263ffffffff61059816565b600160a060020a038084166000818152602081815260409182902094909455805185815290519193928716927fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef92918290030190a3505050565b6000828201838110156105aa57600080fd5b939250505056fea165627a7a72305820722c0187518ce2856a424bdba350d5a263c8f98fcb19cb4cc161372bc3b794c90029",
"deployedBytecode": "0x608060405234801561001057600080fd5b50600436106100a5576000357c01000000000000000000000000000000000000000000000000000000009004806370a082311161007857806370a0823114610166578063a457c2d71461018c578063a9059cbb146101b8578063dd62ed3e146101e4576100a5565b8063095ea7b3146100aa57806318160ddd146100ea57806323b872dd14610104578063395093511461013a575b600080fd5b6100d6600480360360408110156100c057600080fd5b50600160a060020a038135169060200135610212565b604080519115158252519081900360200190f35b6100f2610290565b60408051918252519081900360200190f35b6100d66004803603606081101561011a57600080fd5b50600160a060020a03813581169160208101359091169060400135610296565b6100d66004803603604081101561015057600080fd5b50600160a060020a03813516906020013561035f565b6100f26004803603602081101561017c57600080fd5b5035600160a060020a031661040f565b6100d6600480360360408110156101a257600080fd5b50600160a060020a03813516906020013561042a565b6100d6600480360360408110156101ce57600080fd5b50600160a060020a038135169060200135610475565b6100f2600480360360408110156101fa57600080fd5b50600160a060020a038135811691602001351661048b565b6000600160a060020a038316151561022957600080fd5b336000818152600160209081526040808320600160a060020a03881680855290835292819020869055805186815290519293927f8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925929181900390910190a350600192915050565b60025490565b600160a060020a03831660009081526001602090815260408083203384529091528120546102ca908363ffffffff6104b616565b600160a060020a03851660009081526001602090815260408083203384529091529020556102f98484846104cb565b600160a060020a0384166000818152600160209081526040808320338085529083529281902054815190815290519293927f8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925929181900390910190a35060019392505050565b6000600160a060020a038316151561037657600080fd5b336000908152600160209081526040808320600160a060020a03871684529091529020546103aa908363ffffffff61059816565b336000818152600160209081526040808320600160a060020a0389168085529083529281902085905580519485525191937f8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925929081900390910190a350600192915050565b600160a060020a031660009081526020819052604090205490565b6000600160a060020a038316151561044157600080fd5b336000908152600160209081526040808320600160a060020a03871684529091529020546103aa908363ffffffff6104b616565b60006104823384846104cb565b50600192915050565b600160a060020a03918216600090815260016020908152604080832093909416825291909152205490565b6000828211156104c557600080fd5b50900390565b600160a060020a03821615156104e057600080fd5b600160a060020a038316600090815260208190526040902054610509908263ffffffff6104b616565b600160a060020a03808516600090815260208190526040808220939093559084168152205461053e908263ffffffff61059816565b600160a060020a038084166000818152602081815260409182902094909455805185815290519193928716927fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef92918290030190a3505050565b6000828201838110156105aa57600080fd5b939250505056fea165627a7a72305820722c0187518ce2856a424bdba350d5a263c8f98fcb19cb4cc161372bc3b794c90029",
"compiler": {
"name": "solc",
"version": "0.5.4+commit.9549d8ff.Emscripten.clang"
}
}

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,145 @@
{
"abi": [
{
"inputs": [
{
"components": [
{
"internalType": "address",
"name": "target",
"type": "address"
},
{
"internalType": "bytes",
"name": "callData",
"type": "bytes"
}
],
"internalType": "struct Multicall.Call[]",
"name": "calls",
"type": "tuple[]"
}
],
"name": "aggregate",
"outputs": [
{
"internalType": "uint256",
"name": "blockNumber",
"type": "uint256"
},
{
"internalType": "bytes[]",
"name": "returnData",
"type": "bytes[]"
}
],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "blockNumber",
"type": "uint256"
}
],
"name": "getBlockHash",
"outputs": [
{
"internalType": "bytes32",
"name": "blockHash",
"type": "bytes32"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "getCurrentBlockCoinbase",
"outputs": [
{
"internalType": "address",
"name": "coinbase",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "getCurrentBlockDifficulty",
"outputs": [
{
"internalType": "uint256",
"name": "difficulty",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "getCurrentBlockGasLimit",
"outputs": [
{
"internalType": "uint256",
"name": "gaslimit",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "getCurrentBlockTimestamp",
"outputs": [
{
"internalType": "uint256",
"name": "timestamp",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "addr",
"type": "address"
}
],
"name": "getEthBalance",
"outputs": [
{
"internalType": "uint256",
"name": "balance",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "getLastBlockHash",
"outputs": [
{
"internalType": "bytes32",
"name": "blockHash",
"type": "bytes32"
}
],
"stateMutability": "view",
"type": "function"
}
],
"bytecode": "608060405234801561001057600080fd5b5061066e806100206000396000f3fe608060405234801561001057600080fd5b50600436106100885760003560e01c806372425d9d1161005b57806372425d9d146100e757806386d516e8146100ef578063a8b0574e146100f7578063ee82ac5e1461010c57610088565b80630f28c97d1461008d578063252dba42146100ab57806327e86d6e146100cc5780634d2301cc146100d4575b600080fd5b61009561011f565b6040516100a2919061051e565b60405180910390f35b6100be6100b93660046103b6565b610123565b6040516100a292919061052c565b610095610231565b6100956100e2366004610390565b61023a565b610095610247565b61009561024b565b6100ff61024f565b6040516100a2919061050a565b61009561011a3660046103eb565b610253565b4290565b60006060439150825160405190808252806020026020018201604052801561015f57816020015b606081526020019060019003908161014a5790505b50905060005b835181101561022b576000606085838151811061017e57fe5b6020026020010151600001516001600160a01b031686848151811061019f57fe5b6020026020010151602001516040516101b891906104fe565b6000604051808303816000865af19150503d80600081146101f5576040519150601f19603f3d011682016040523d82523d6000602084013e6101fa565b606091505b50915091508161020957600080fd5b8084848151811061021657fe5b60209081029190910101525050600101610165565b50915091565b60001943014090565b6001600160a01b03163190565b4490565b4590565b4190565b4090565b600061026382356105d4565b9392505050565b600082601f83011261027b57600080fd5b813561028e61028982610573565b61054c565b81815260209384019390925082018360005b838110156102cc57813586016102b68882610325565b84525060209283019291909101906001016102a0565b5050505092915050565b600082601f8301126102e757600080fd5b81356102f561028982610594565b9150808252602083016020830185838301111561031157600080fd5b61031c8382846105ee565b50505092915050565b60006040828403121561033757600080fd5b610341604061054c565b9050600061034f8484610257565b825250602082013567ffffffffffffffff81111561036c57600080fd5b610378848285016102d6565b60208301525092915050565b600061026382356105df565b6000602082840312156103a257600080fd5b60006103ae8484610257565b949350505050565b6000602082840312156103c857600080fd5b813567ffffffffffffffff8111156103df57600080fd5b6103ae8482850161026a565b6000602082840312156103fd57600080fd5b60006103ae8484610384565b60006102638383610497565b61041e816105d4565b82525050565b600061042f826105c2565b61043981856105c6565b93508360208202850161044b856105bc565b60005b84811015610482578383038852610466838351610409565b9250610471826105bc565b60209890980197915060010161044e565b50909695505050505050565b61041e816105df565b60006104a2826105c2565b6104ac81856105c6565b93506104bc8185602086016105fa565b6104c58161062a565b9093019392505050565b60006104da826105c2565b6104e481856105cf565b93506104f48185602086016105fa565b9290920192915050565b600061026382846104cf565b602081016105188284610415565b92915050565b60208101610518828461048e565b6040810161053a828561048e565b81810360208301526103ae8184610424565b60405181810167ffffffffffffffff8111828210171561056b57600080fd5b604052919050565b600067ffffffffffffffff82111561058a57600080fd5b5060209081020190565b600067ffffffffffffffff8211156105ab57600080fd5b506020601f91909101601f19160190565b60200190565b5190565b90815260200190565b919050565b6000610518826105e2565b90565b6001600160a01b031690565b82818337506000910152565b60005b838110156106155781810151838201526020016105fd565b83811115610624576000848401525b50505050565b601f01601f19169056fea265627a7a72305820978cd44d5ce226bebdf172bdf24918753b9e111e3803cb6249d3ca2860b7a47f6c6578706572696d656e74616cf50037"
}

View File

@ -0,0 +1,6 @@
import VotingContract from './../build/VotingContract.json'
import ERC20 from './ERC20.json'
import ERC20Mock from './ERC20Mock.json'
import MultiCall from './MultiCall.json'
export { VotingContract, ERC20, MultiCall, ERC20Mock }

View File

@ -0,0 +1,122 @@
pragma solidity ^0.8.5;
import '@openzeppelin/contracts/token/ERC20/IERC20.sol';
import '@openzeppelin/contracts/utils/cryptography/ECDSA.sol';
import '@openzeppelin/contracts/utils/math/SafeMath.sol';
contract VotingContract {
using SafeMath for uint256;
IERC20 public token;
uint256 private constant VOTING_LENGTH = 1000;
bytes32 private constant EIP712DOMAIN_TYPEHASH =
keccak256('EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)');
bytes32 private constant VOTE_TYPEHASH = keccak256('Vote(uint256 roomIdAndType,uint256 tokenAmount,address voter)');
bytes32 private DOMAIN_SEPARATOR;
struct VotingRoom {
uint256 startBlock;
uint256 endAt;
string question;
string description;
uint256 totalVotesFor;
uint256 totalVotesAgainst;
address[] voters;
}
mapping(uint256 => mapping(address => bool)) private voted;
VotingRoom[] public votingRooms;
struct Vote {
address voter;
uint256 roomIdAndType;
uint256 tokenAmount;
bytes32 r;
bytes32 vs;
}
event VoteCast(uint256 roomId, address voter);
event NotEnoughToken(uint256 roomId, address voter);
event VotingRoomStarted(uint256 roomId, string question);
constructor(IERC20 _address) {
token = _address;
DOMAIN_SEPARATOR = keccak256(
abi.encode(
EIP712DOMAIN_TYPEHASH,
keccak256(bytes('Voting Contract')),
keccak256(bytes('1')),
block.chainid,
address(this)
)
);
}
function getVotingRooms() public view returns (VotingRoom[] memory) {
return votingRooms;
}
function listRoomVoters(uint256 roomId) public view returns (address[] memory) {
require(roomId < votingRooms.length, 'No room');
return votingRooms[roomId].voters;
}
function initializeVotingRoom(
string calldata question,
string calldata description,
uint256 voteAmount
) public {
require(token.balanceOf(msg.sender) >= voteAmount, 'not enough token');
VotingRoom memory newVotingRoom;
newVotingRoom.startBlock = block.number;
newVotingRoom.endAt = block.timestamp.add(VOTING_LENGTH);
newVotingRoom.question = question;
newVotingRoom.description = description;
newVotingRoom.totalVotesFor = voteAmount;
voted[votingRooms.length][msg.sender] = true;
votingRooms.push(newVotingRoom);
votingRooms[votingRooms.length - 1].voters.push(msg.sender);
emit VotingRoomStarted(votingRooms.length - 1, question);
}
function verify(
Vote calldata vote,
bytes32 r,
bytes32 vs
) internal view returns (bool) {
bytes32 voteHash = keccak256(abi.encode(VOTE_TYPEHASH, vote.roomIdAndType, vote.tokenAmount, vote.voter));
bytes32 digest = keccak256(abi.encodePacked('\x19\x01', DOMAIN_SEPARATOR, voteHash));
return ECDSA.recover(digest, r, vs) == vote.voter;
}
function updateRoomVotes(Vote calldata vote, uint256 roomId) internal {
if (vote.roomIdAndType & 1 == 1) {
votingRooms[roomId].totalVotesFor = votingRooms[roomId].totalVotesFor.add(vote.tokenAmount);
} else {
votingRooms[roomId].totalVotesAgainst = votingRooms[roomId].totalVotesAgainst.add(vote.tokenAmount);
}
votingRooms[roomId].voters.push(vote.voter);
voted[roomId][vote.voter] = true;
}
function castVotes(Vote[] calldata votes) public {
for (uint256 i = 0; i < votes.length; i++) {
Vote calldata vote = votes[i];
uint256 roomId = vote.roomIdAndType >> 1;
require(votingRooms[roomId].endAt > block.timestamp, 'vote closed');
require(roomId < votingRooms.length, 'vote not found');
if (verify(vote, vote.r, vote.vs)) {
if (voted[roomId][vote.voter] == false) {
if (token.balanceOf(vote.voter) >= vote.tokenAmount) {
updateRoomVotes(vote, roomId);
emit VoteCast(roomId, vote.voter);
} else {
emit NotEnoughToken(roomId, vote.voter);
}
}
}
}
}
}

View File

@ -0,0 +1,19 @@
import { ethers } from 'ethers'
import { deployContract } from 'ethereum-waffle'
import VotingContract from '../build/VotingContract.json'
const deploy = async () => {
const providerName = process.env.ETHEREUM_PROVIDER
const privateKey = process.env.ETHEREUM_PRIVATE_KEY
if (privateKey && providerName) {
console.log(`deploying on ${providerName}`)
const provider = ethers.getDefaultProvider(process.env.ETHEREUM_PROVIDER)
const wallet = new ethers.Wallet(privateKey, provider)
const mockContract = await deployContract(wallet, VotingContract)
console.log(`Contract deployed with address: ${mockContract.address}`)
}
}
deploy()

View File

@ -0,0 +1,22 @@
import { ethers } from 'ethers'
import { deployContract } from 'ethereum-waffle'
import { VotingContract, Directory } from '../abi'
const deploy = async () => {
const providerName = process.env.ETHEREUM_PROVIDER
const privateKey = process.env.ETHEREUM_PRIVATE_KEY
console.log(privateKey)
if (privateKey && providerName) {
console.log(`deploying on ${providerName}`)
const provider = ethers.getDefaultProvider(process.env.ETHEREUM_PROVIDER)
const wallet = new ethers.Wallet(privateKey, provider)
const votingContract = await deployContract(wallet, VotingContract,[process.env.ETHEREUM_TOKEN_ADDRESS])
console.log(`Voting contract deployed with address: ${votingContract.address}`)
}
}
deploy()

View File

@ -0,0 +1,25 @@
import { BigNumber, ethers } from 'ethers'
import { deployContract } from 'ethereum-waffle'
import {VotingContract, Directory, ERC20Mock} from '../abi'
const deploy = async () => {
const providerName = process.env.ETHEREUM_PROVIDER
const privateKey = process.env.ETHEREUM_PRIVATE_KEY
console.log(privateKey)
if (privateKey && providerName) {
console.log(`deploying on ${providerName}`)
const provider = ethers.getDefaultProvider(process.env.ETHEREUM_PROVIDER)
const wallet = new ethers.Wallet(privateKey, provider)
const ercArgs = ['MSNT', 'Mock SNT', wallet.address, BigNumber.from('0x33B2E3C9FD0803CE8000000')]
const erc20 = await deployContract(wallet,ERC20Mock,ercArgs)
console.log(`ERC20 Token deployed with address: ${erc20.address}`)
const votingContract = await deployContract(wallet, VotingContract,[erc20.address])
console.log(`Voting contract deployed with address: ${votingContract.address}`)
}
}
deploy()

View File

@ -0,0 +1,21 @@
import { ethers } from 'ethers'
import { deployContract } from 'ethereum-waffle'
import { MultiCall } from '../abi'
const deploy = async () => {
const providerName = process.env.ETHEREUM_PROVIDER
const privateKey = process.env.ETHEREUM_PRIVATE_KEY
console.log(privateKey)
if (privateKey && providerName) {
console.log(`deploying on ${providerName}`)
const provider = ethers.getDefaultProvider(process.env.ETHEREUM_PROVIDER)
const wallet = new ethers.Wallet(privateKey, provider)
const multiCall = await deployContract(wallet, MultiCall)
console.log(`MultiCall deployed with address: ${multiCall.address}`)
}
}
deploy()

View File

@ -0,0 +1,39 @@
{
"name": "@status-community-dapp/contracts",
"version": "0.0.1",
"main": "index.js",
"repository": "git@github.com:EthWorks/status-community-dapp.git",
"license": "MIT",
"scripts": {
"clean": "rm -r ./{build,dist,flatten}",
"build:sol": "waffle waffle.config.json",
"build:ts": "tsc",
"build": "yarn build:sol && yarn build:ts",
"lint": "yarn lint:prettier --check && yarn lint:solhint && yarn lint:eslint",
"lint:fix": "yarn lint:prettier --write && yarn lint:eslint --fix",
"lint:eslint": "eslint './test/**/*.ts'",
"lint:prettier": "yarn prettier './{contracts,test}/**/*.{ts,sol}'",
"lint:solhint": "yarn solhint -f table contracts/**/*.sol",
"flatten": "waffle flatten",
"test": "mocha -r ts-node/register/transpile-only 'test/**/*.test.ts'"
},
"devDependencies": {
"@openzeppelin/contracts": "^4.1.0",
"@types/chai": "^4.2.18",
"@types/mocha": "^8.2.2",
"chai": "^4.3.4",
"eslint": "^7.28.0",
"ethereum-waffle": "^3.3.0",
"mocha": "^9.0.0",
"prettier": "^2.3.1",
"prettier-plugin-solidity": "^1.0.0-beta.13",
"solhint": "^3.3.6",
"solhint-plugin-prettier": "^0.0.5",
"ts-node": "^10.0.0",
"typescript": "^4.3.3"
},
"dependencies": {
"eth-sig-util": "^3.0.1",
"ethers": "5.4.1"
}
}

View File

@ -0,0 +1 @@
module.exports = require('../../.prettierrc.json')

View File

@ -0,0 +1,279 @@
import { expect, use } from 'chai'
import { loadFixture, deployContract, MockProvider, solidity } from 'ethereum-waffle'
import { VotingContract, ERC20Mock } from '../abi'
import { utils, Wallet, Contract } from 'ethers'
import { signTypedMessage, TypedMessage } from 'eth-sig-util'
import { BigNumber } from '@ethersproject/bignumber'
use(solidity)
interface MessageTypeProperty {
name: string
type: string
}
interface MessageTypes {
EIP712Domain: MessageTypeProperty[]
[additionalProperties: string]: MessageTypeProperty[]
}
const typedData = {
types: {
EIP712Domain: [
{ name: 'name', type: 'string' },
{ name: 'version', type: 'string' },
{ name: 'chainId', type: 'uint256' },
{ name: 'verifyingContract', type: 'address' },
],
Vote: [
{ name: 'roomIdAndType', type: 'uint256' },
{ name: 'tokenAmount', type: 'uint256' },
{ name: 'voter', type: 'address' },
],
},
primaryType: 'Vote',
domain: {
name: 'Voting Contract',
version: '1',
chainId: 0,
verifyingContract: '',
},
}
const getSignedMessages = async (
alice: Wallet,
firstAddress: Wallet,
secondAddress: Wallet
): Promise<{ messages: any[]; signedMessages: any[] }> => {
const votes = [
{
voter: alice,
vote: 1,
tokenAmount: BigNumber.from(100),
sessionID: 0,
},
{
voter: firstAddress,
vote: 0,
tokenAmount: BigNumber.from(100),
sessionID: 0,
},
{
voter: secondAddress,
vote: 1,
tokenAmount: BigNumber.from(100),
sessionID: 0,
},
]
const types = ['address', 'uint256', 'uint256']
const messages = votes.map((vote) => {
return [vote.voter.address, BigNumber.from(vote.sessionID).mul(2).add(vote.vote), vote.tokenAmount] as [
string,
BigNumber,
BigNumber
]
})
const signedMessages = messages.map((msg, idx) => {
const t: TypedMessage<MessageTypes> = {
...typedData,
message: { roomIdAndType: msg[1].toHexString(), tokenAmount: msg[2].toHexString(), voter: msg[0] },
}
const sig = utils.splitSignature(
signTypedMessage(Buffer.from(utils.arrayify(votes[idx].voter.privateKey)), { data: t }, 'V3')
)
return [...msg, sig.r, sig._vs]
})
return { messages, signedMessages }
}
async function fixture([alice, firstAddress, secondAddress]: any[], provider: any) {
const erc20 = await deployContract(alice, ERC20Mock, ['MSNT', 'Mock SNT', alice.address, 100000])
await erc20.transfer(firstAddress.address, 10000)
await erc20.transfer(secondAddress.address, 10000)
const contract = await deployContract(alice, VotingContract, [erc20.address])
await provider.send('evm_mine', [Math.floor(Date.now() / 1000)])
return { contract, alice, firstAddress, secondAddress, provider }
}
before(async () => {
const { contract } = await loadFixture(fixture)
typedData.domain.chainId = 1
typedData.domain.verifyingContract = contract.address
})
describe('Contract', () => {
describe('Voting Room', () => {
describe('initialization', () => {
it('initializes', async () => {
const { contract } = await loadFixture(fixture)
await expect(await contract.initializeVotingRoom('test', 'short desc', BigNumber.from(100)))
.to.emit(contract, 'VotingRoomStarted')
.withArgs(0, 'test')
await expect(await contract.initializeVotingRoom('test2', 'short desc', BigNumber.from(100)))
.to.emit(contract, 'VotingRoomStarted')
.withArgs(1, 'test2')
})
it('not enough token', async () => {
const { contract } = await loadFixture(fixture)
await expect(
contract.initializeVotingRoom('test', 'short desc', BigNumber.from(10000000000000))
).to.be.revertedWith('not enough token')
})
})
it('gets', async () => {
const { contract } = await loadFixture(fixture)
await contract.initializeVotingRoom('T1', 'short desc', BigNumber.from(100))
expect((await contract.votingRooms(0))[2]).to.eq('T1')
expect((await contract.votingRooms(0))[3]).to.eq('short desc')
expect((await contract.votingRooms(0))[4]).to.deep.eq(BigNumber.from(100))
expect((await contract.votingRooms(0))[5]).to.deep.eq(BigNumber.from(0))
await contract.initializeVotingRoom('T2', 'long desc', BigNumber.from(200))
expect((await contract.votingRooms(1))[2]).to.eq('T2')
expect((await contract.votingRooms(1))[3]).to.eq('long desc')
expect((await contract.votingRooms(1))[4]).to.deep.eq(BigNumber.from(200))
expect((await contract.votingRooms(1))[5]).to.deep.eq(BigNumber.from(0))
})
it('reverts no room', async () => {
const { contract } = await loadFixture(fixture)
await expect(contract.votingRooms(1)).to.be.reverted
await expect(contract.votingRooms(0)).to.be.reverted
await contract.initializeVotingRoom('T2', '', BigNumber.from(200))
await expect(contract.votingRooms(1)).to.be.reverted
})
})
describe('helpers', () => {
it('get voting rooms', async () => {
const { contract, firstAddress, secondAddress, provider } = await loadFixture(fixture)
await contract.initializeVotingRoom('T1', 't1', BigNumber.from(100))
await contract.initializeVotingRoom('T2', 't2', BigNumber.from(200))
const votingRooms = await contract.getVotingRooms()
expect(votingRooms.length).to.eq(2)
expect(votingRooms[0][2]).to.eq('T1')
expect(votingRooms[0][3]).to.eq('t1')
expect(votingRooms[0][4]).to.deep.eq(BigNumber.from(100))
expect(votingRooms[0][5]).to.deep.eq(BigNumber.from(0))
expect(votingRooms[1][2]).to.eq('T2')
expect(votingRooms[1][3]).to.eq('t2')
expect(votingRooms[1][4]).to.deep.eq(BigNumber.from(200))
expect(votingRooms[1][5]).to.deep.eq(BigNumber.from(0))
})
})
describe('voting', () => {
it('check voters', async () => {
const { contract, alice, firstAddress, secondAddress } = await loadFixture(fixture)
const { signedMessages } = await getSignedMessages(alice, firstAddress, secondAddress)
await contract.initializeVotingRoom('0xabA1eF51ef4aE360a9e8C9aD2d787330B602eb24', '', BigNumber.from(100))
expect(await contract.listRoomVoters(0)).to.deep.eq([alice.address])
await contract.castVotes(signedMessages.slice(2))
expect(await contract.listRoomVoters(0)).to.deep.eq([alice.address, secondAddress.address])
})
it('not enough tokens', async () => {
const { contract, firstAddress } = await loadFixture(fixture)
await contract.initializeVotingRoom('test', '', BigNumber.from(100))
const msg = [firstAddress.address, BigNumber.from(0).mul(2).add(1), BigNumber.from(100000000000)]
const t: TypedMessage<MessageTypes> = {
...typedData,
message: { roomIdAndType: msg[1].toHexString(), tokenAmount: msg[2].toHexString(), voter: msg[0] },
}
const sig = utils.splitSignature(
signTypedMessage(Buffer.from(utils.arrayify(firstAddress.privateKey)), { data: t }, 'V3')
)
const signedMessage = [...msg, sig.r, sig._vs]
await expect(await contract.castVotes([signedMessage]))
.to.emit(contract, 'NotEnoughToken')
.withArgs(0, firstAddress.address)
const votingRoom = await contract.votingRooms(0)
expect(votingRoom[2]).to.eq('test')
expect(votingRoom[3]).to.eq('')
expect(votingRoom[4]).to.deep.eq(BigNumber.from(100))
expect(votingRoom[5]).to.deep.eq(BigNumber.from(0))
})
it('success', async () => {
const { contract, alice, firstAddress, secondAddress } = await loadFixture(fixture)
const { signedMessages } = await getSignedMessages(alice, firstAddress, secondAddress)
await contract.initializeVotingRoom('test', 'test desc', BigNumber.from(100))
await contract.castVotes(signedMessages)
const votingRoom = await contract.votingRooms(0)
expect(votingRoom[2]).to.eq('test')
expect(votingRoom[3]).to.eq('test desc')
expect(votingRoom[4]).to.deep.eq(BigNumber.from(200))
expect(votingRoom[5]).to.deep.eq(BigNumber.from(100))
})
it('double vote', async () => {
const { contract, alice, firstAddress, secondAddress } = await loadFixture(fixture)
const { signedMessages } = await getSignedMessages(alice, firstAddress, secondAddress)
await contract.initializeVotingRoom('test', '', BigNumber.from(100))
await contract.castVotes(signedMessages)
await contract.castVotes(signedMessages)
const votingRoom = await contract.votingRooms(0)
expect(votingRoom[2]).to.eq('test')
expect(votingRoom[3]).to.eq('')
expect(votingRoom[4]).to.deep.eq(BigNumber.from(200))
expect(votingRoom[5]).to.deep.eq(BigNumber.from(100))
})
it('random bytes', async () => {
const { contract } = await loadFixture(fixture)
await contract.initializeVotingRoom('test', '', BigNumber.from(100))
await expect(contract.castVotes([new Uint8Array([12, 12, 12])])).to.be.reverted
})
it('none existent room', async () => {
const { contract, alice, firstAddress, secondAddress } = await loadFixture(fixture)
const { signedMessages } = await getSignedMessages(alice, firstAddress, secondAddress)
await expect(contract.castVotes(signedMessages)).to.be.reverted
})
it('old room', async () => {
const { contract, alice, firstAddress, secondAddress, provider } = await loadFixture(fixture)
const { signedMessages } = await getSignedMessages(alice, firstAddress, secondAddress)
await contract.initializeVotingRoom('test', '', BigNumber.from(100))
await provider.send('evm_mine', [Math.floor(Date.now() / 1000 + 2000)])
await expect(contract.castVotes(signedMessages)).to.be.reverted
})
it('wrong signature', async () => {
const { contract, alice, firstAddress, secondAddress, provider } = await loadFixture(fixture)
const { messages } = await getSignedMessages(alice, firstAddress, secondAddress)
await contract.initializeVotingRoom('test', '', BigNumber.from(100))
await provider.send('evm_mine', [Math.floor(Date.now() / 1000 + 2000)])
const signedMessages = await Promise.all(
messages.map(async (msg) => {
const t: TypedMessage<MessageTypes> = {
...typedData,
message: { roomIdAndType: msg[1].toHexString(), tokenAmount: msg[2].toHexString(), voter: msg[0] },
}
const sig = utils.splitSignature(
signTypedMessage(Buffer.from(utils.arrayify(firstAddress.privateKey)), { data: t }, 'V3')
)
const signedMessage = [...msg, sig.r, sig._vs]
return signedMessage
})
)
await expect(contract.castVotes(signedMessages)).to.be.reverted
})
})
})

View File

@ -0,0 +1,24 @@
{
"compilerOptions": {
"esModuleInterop": true,
"lib": [
"esnext",
],
"module": "commonjs",
"resolveJsonModule": true,
"skipLibCheck": true,
"strict": true,
"sourceMap": true,
"target": "es6",
"outDir": "dist",
"downlevelIteration": true
},
"include": [
"test",
"contracts"
],
"exclude": [
"**/dist",
"**/build"
]
}

View File

@ -0,0 +1,8 @@
{
"compilerType": "solcjs",
"compilerVersion": "0.8.5",
"sourceDirectory": "./contracts",
"outputDirectory": "./build",
"nodeModulesDirectory": "./node_modules",
"flattenOutputDirectory": "./flatten"
}

1109
yarn.lock

File diff suppressed because it is too large Load Diff