mirror of
https://github.com/status-im/dappconnect-vote-poll-sdk.git
synced 2025-02-16 15:17:35 +00:00
Add polling contract (#35)
This commit is contained in:
parent
aae698c9b7
commit
0237513e5a
5
packages/contracts/.eslintrc.json
Normal file
5
packages/contracts/.eslintrc.json
Normal file
@ -0,0 +1,5 @@
|
||||
{
|
||||
"extends": [
|
||||
"../../.eslintrc.json"
|
||||
]
|
||||
}
|
10
packages/contracts/.solhint.json
Normal file
10
packages/contracts/.solhint.json
Normal 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}]
|
||||
}
|
||||
}
|
186
packages/contracts/README.md
Normal file
186
packages/contracts/README.md
Normal 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 ?
|
230
packages/contracts/abi/ERC20.json
Normal file
230
packages/contracts/abi/ERC20.json
Normal 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"
|
||||
}
|
||||
}
|
389
packages/contracts/abi/ERC20Mock.json
Normal file
389
packages/contracts/abi/ERC20Mock.json
Normal file
File diff suppressed because one or more lines are too long
145
packages/contracts/abi/MultiCall.json
Normal file
145
packages/contracts/abi/MultiCall.json
Normal 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"
|
||||
}
|
||||
|
6
packages/contracts/abi/index.ts
Normal file
6
packages/contracts/abi/index.ts
Normal 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 }
|
122
packages/contracts/contracts/VotingContract.sol
Normal file
122
packages/contracts/contracts/VotingContract.sol
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
19
packages/contracts/deploy/VotingContract.ts
Normal file
19
packages/contracts/deploy/VotingContract.ts
Normal 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()
|
||||
|
22
packages/contracts/deploy/deploy.ts
Normal file
22
packages/contracts/deploy/deploy.ts
Normal 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()
|
||||
|
25
packages/contracts/deploy/deployTestnet.ts
Normal file
25
packages/contracts/deploy/deployTestnet.ts
Normal 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()
|
||||
|
21
packages/contracts/deploy/multicall.ts
Normal file
21
packages/contracts/deploy/multicall.ts
Normal 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()
|
||||
|
39
packages/contracts/package.json
Normal file
39
packages/contracts/package.json
Normal 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"
|
||||
}
|
||||
}
|
1
packages/contracts/prettier.config.js
Normal file
1
packages/contracts/prettier.config.js
Normal file
@ -0,0 +1 @@
|
||||
module.exports = require('../../.prettierrc.json')
|
279
packages/contracts/test/1.votingContract.test.ts
Normal file
279
packages/contracts/test/1.votingContract.test.ts
Normal 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
|
||||
})
|
||||
})
|
||||
})
|
24
packages/contracts/tsconfig.json
Normal file
24
packages/contracts/tsconfig.json
Normal 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"
|
||||
]
|
||||
}
|
8
packages/contracts/waffle.config.json
Normal file
8
packages/contracts/waffle.config.json
Normal file
@ -0,0 +1,8 @@
|
||||
{
|
||||
"compilerType": "solcjs",
|
||||
"compilerVersion": "0.8.5",
|
||||
"sourceDirectory": "./contracts",
|
||||
"outputDirectory": "./build",
|
||||
"nodeModulesDirectory": "./node_modules",
|
||||
"flattenOutputDirectory": "./flatten"
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user