feat: voting (#7)
* feat: add voting functionality * adding tests pt 1 * additional tests for staking pool dao * fix: remove whitespace
This commit is contained in:
parent
c4d7a9f3c3
commit
e88796da46
|
@ -64,8 +64,7 @@ contract StakingPool is ERC20, ERC20Detailed, ERC20Burnable, DSMath, ApproveAndC
|
|||
_stake(msg.sender, _amount);
|
||||
MAX_SUPPLY = totalSupply();
|
||||
} else {
|
||||
uint maxAmountToStake = MAX_SUPPLY - totalSupply();
|
||||
require(_amount <= maxAmountToStake, "Max stake amount exceeded");
|
||||
require(_amount <= (MAX_SUPPLY - totalSupply()), "Max stake amount exceeded");
|
||||
_stake(msg.sender, _amount);
|
||||
}
|
||||
}
|
||||
|
@ -121,7 +120,7 @@ contract StakingPool is ERC20, ERC20Detailed, ERC20Burnable, DSMath, ApproveAndC
|
|||
* @dev Decode calldata - stake(uint256)
|
||||
* @param _data Calldata, ABI encoded
|
||||
*/
|
||||
function abiDecode(bytes memory _data) internal returns(
|
||||
function abiDecode(bytes memory _data) internal pure returns (
|
||||
bytes4 sig,
|
||||
uint256 amount
|
||||
) {
|
||||
|
|
|
@ -0,0 +1,167 @@
|
|||
|
||||
/* solium-disable security/no-block-members */
|
||||
/* solium-disable security/no-inline-assembly */
|
||||
pragma solidity >=0.5.0 <0.6.0;
|
||||
|
||||
import "./StakingPool.sol";
|
||||
import "./common/Controlled.sol";
|
||||
import "openzeppelin-solidity/contracts/drafts/ERC20Snapshot.sol";
|
||||
|
||||
contract StakingPoolDAO is StakingPool, ERC20Snapshot, Controlled {
|
||||
|
||||
enum VoteStatus {NONE, YES, NO}
|
||||
|
||||
struct Proposal {
|
||||
address destination;
|
||||
uint value;
|
||||
bool executed;
|
||||
uint snapshotId;
|
||||
uint voteEndingBlock;
|
||||
bytes data;
|
||||
bytes details; // Store proposal information here
|
||||
|
||||
mapping(bool => uint) votes;
|
||||
mapping(address => VoteStatus) voters;
|
||||
}
|
||||
|
||||
uint public proposalCount;
|
||||
mapping(uint => Proposal) public proposals;
|
||||
|
||||
uint public proposalVoteLength; // Voting available during this period
|
||||
uint public proposalExpirationLength; // Proposals should be executed up to 1 day after they have ended
|
||||
|
||||
|
||||
event NewProposal(uint indexed proposalId);
|
||||
event Vote(uint indexed proposalId, address indexed voter, VoteStatus indexed choice);
|
||||
event Execution(uint indexed proposalId);
|
||||
event ExecutionFailure(uint indexed proposalId);
|
||||
|
||||
constructor (address _tokenAddress, uint _stakingPeriodLen, uint _proposalVoteLength, uint _proposalExpirationLength) public
|
||||
StakingPool(_tokenAddress, _stakingPeriodLen) {
|
||||
changeController(address(uint160(address(this))));
|
||||
proposalVoteLength = _proposalVoteLength;
|
||||
proposalExpirationLength = _proposalExpirationLength;
|
||||
}
|
||||
|
||||
function setProposalVoteLength(uint _newProposalVoteLength) public onlyController {
|
||||
proposalVoteLength = _newProposalVoteLength;
|
||||
}
|
||||
|
||||
function setproposalExpirationLength(uint _newProposalExpirationLength) public onlyController {
|
||||
proposalExpirationLength = _newProposalExpirationLength;
|
||||
}
|
||||
|
||||
/// @dev Adds a new proposal
|
||||
/// @param destination Transaction target address.
|
||||
/// @param value Transaction ether value.
|
||||
/// @param data Transaction data payload.
|
||||
/// @param details Proposal details
|
||||
/// @return Returns proposal ID.
|
||||
function addProposal(address destination, uint value, bytes calldata data, bytes calldata details) external returns (uint proposalId)
|
||||
{
|
||||
require(balanceOf(msg.sender) > 0, "Token balance is required to perform this operation");
|
||||
|
||||
// TODO: should proposals have a cost? or require a minimum amount of tokens?
|
||||
|
||||
assert(destination != address(0));
|
||||
|
||||
proposalId = proposalCount;
|
||||
proposals[proposalId] = Proposal({
|
||||
destination: destination,
|
||||
value: value,
|
||||
data: data,
|
||||
executed: false,
|
||||
snapshotId: snapshot(),
|
||||
details: details,
|
||||
voteEndingBlock: block.number + proposalVoteLength
|
||||
});
|
||||
|
||||
proposalCount++;
|
||||
|
||||
emit NewProposal(proposalId);
|
||||
}
|
||||
|
||||
function vote(uint proposalId, bool choice) external {
|
||||
Proposal storage proposal = proposals[proposalId];
|
||||
|
||||
require(proposal.voteEndingBlock > block.number, "Proposal has already ended");
|
||||
|
||||
uint voterBalance = balanceOfAt(msg.sender, proposal.snapshotId);
|
||||
require(voterBalance > 0, "Not enough tokens at the moment of proposal creation");
|
||||
|
||||
VoteStatus oldVote = proposal.voters[msg.sender];
|
||||
|
||||
if(oldVote != VoteStatus.NONE){ // Reset
|
||||
bool oldChoice = oldVote == VoteStatus.YES ? true : false;
|
||||
proposal.votes[oldChoice] -= voterBalance;
|
||||
}
|
||||
|
||||
VoteStatus enumVote = choice ? VoteStatus.YES : VoteStatus.NO;
|
||||
|
||||
proposal.votes[choice] += voterBalance;
|
||||
proposal.voters[msg.sender] = enumVote;
|
||||
|
||||
emit Vote(proposalId, msg.sender, enumVote);
|
||||
}
|
||||
|
||||
// call has been separated into its own function in order to take advantage
|
||||
// of the Solidity's code generator to produce a loop that copies tx.data into memory.
|
||||
function external_call(address destination, uint value, uint dataLength, bytes memory data) internal returns (bool) {
|
||||
bool result;
|
||||
assembly {
|
||||
let x := mload(0x40) // "Allocate" memory for output (0x40 is where "free memory" pointer is stored by convention)
|
||||
let d := add(data, 32) // First 32 bytes are the padded length of data, so exclude that
|
||||
result := call(
|
||||
sub(gas, 34710), // 34710 is the value that solidity is currently emitting
|
||||
// It includes callGas (700) + callVeryLow (3, to pay for SUB) + callValueTransferGas (9000) +
|
||||
// callNewAccountGas (25000, in case the destination address does not exist and needs creating)
|
||||
destination,
|
||||
value,
|
||||
d,
|
||||
dataLength, // Size of the input (in bytes) - this is what fixes the padding problem
|
||||
x,
|
||||
0 // Output is ignored, therefore the output size is zero
|
||||
)
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/// @dev Allows anyone to execute an approved non-expired proposal
|
||||
/// @param proposalId Proposal ID.
|
||||
function executeTransaction(uint proposalId) public {
|
||||
Proposal storage proposal = proposals[proposalId];
|
||||
|
||||
require(proposal.executed == false, "Proposal already executed");
|
||||
require(block.number > proposal.voteEndingBlock, "Voting is still active");
|
||||
require(block.number <= proposal.voteEndingBlock + proposalExpirationLength, "Proposal is already expired");
|
||||
require(proposal.votes[true] > proposal.votes[false], "Proposal wasn't approved");
|
||||
|
||||
proposal.executed = true;
|
||||
|
||||
bool result = external_call(proposal.destination, proposal.value, proposal.data.length, proposal.data);
|
||||
require(result, "Execution Failed");
|
||||
emit Execution(proposalId);
|
||||
}
|
||||
|
||||
function votes(uint proposalId, bool choice) public view returns (uint) {
|
||||
return proposals[proposalId].votes[choice];
|
||||
}
|
||||
|
||||
function voteOf(address account, uint proposalId) public view returns (VoteStatus) {
|
||||
return proposals[proposalId].voters[account];
|
||||
}
|
||||
|
||||
function isProposalApproved(uint proposalId) public view returns (bool approved, bool executed){
|
||||
Proposal storage proposal = proposals[proposalId];
|
||||
if(block.number <= proposal.voteEndingBlock) {
|
||||
approved = false;
|
||||
} else {
|
||||
approved = proposal.votes[true] > proposal.votes[false];
|
||||
}
|
||||
executed = proposal.executed;
|
||||
}
|
||||
|
||||
function() external payable {
|
||||
//
|
||||
}
|
||||
}
|
|
@ -28,6 +28,6 @@
|
|||
"embark-solium": "0.1.0",
|
||||
"embarkjs": "5.2.3",
|
||||
"embarkjs-web3": "5.2.3",
|
||||
"openzeppelin-solidity": "^2.3.0"
|
||||
"openzeppelin-solidity": "^2.5.0"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,272 @@
|
|||
// /*global contract, config, it, assert, artifacts*/
|
||||
let StakingPoolDAO = artifacts.require('StakingPoolDAO');
|
||||
const SNT = artifacts.require('SNT');
|
||||
|
||||
let iuri, jonathan, richard, michael, pascal, eric, andre;
|
||||
const VoteStatus = {
|
||||
NONE: 0,
|
||||
YES: 1,
|
||||
NO: 2
|
||||
};
|
||||
|
||||
// For documentation please see https://embark.status.im/docs/contracts_testing.html
|
||||
config({
|
||||
contracts: {
|
||||
deploy:
|
||||
{
|
||||
"MiniMeToken": {"deploy": false},
|
||||
"MiniMeTokenFactory": {},
|
||||
"SNT": {
|
||||
"instanceOf": "MiniMeToken",
|
||||
"args": [
|
||||
"$MiniMeTokenFactory",
|
||||
"0x0000000000000000000000000000000000000000",
|
||||
0,
|
||||
"TestMiniMeToken",
|
||||
18,
|
||||
"STT",
|
||||
true
|
||||
]
|
||||
},
|
||||
"StakingPoolDAO": {
|
||||
"deploy": false,
|
||||
"args": ["$SNT"]
|
||||
}
|
||||
}
|
||||
}
|
||||
}, (_err, accounts) => {
|
||||
iuri = accounts[0];
|
||||
jonathan = accounts[1];
|
||||
richard = accounts[2];
|
||||
pascal = accounts[3];
|
||||
michael = accounts[4];
|
||||
eric = accounts[5];
|
||||
andre = accounts[6];
|
||||
});
|
||||
|
||||
// TODO: add asserts for balances
|
||||
|
||||
let StakingPool;
|
||||
|
||||
contract("StakingPoolDAO", function () {
|
||||
this.timeout(0);
|
||||
|
||||
before(async () => {
|
||||
// distribute SNT
|
||||
await SNT.methods.generateTokens(iuri, "10000000000").send({from: iuri});
|
||||
await SNT.methods.generateTokens(jonathan, "10000000000").send({from: iuri});
|
||||
await SNT.methods.generateTokens(richard, "10000000000").send({from: iuri});
|
||||
await SNT.methods.generateTokens(pascal, "10000000000").send({from: iuri});
|
||||
await SNT.methods.generateTokens(michael, "10000000000").send({from: iuri});
|
||||
await SNT.methods.generateTokens(eric, "10000000000").send({from: iuri});
|
||||
|
||||
|
||||
// Deploy Staking Pool
|
||||
StakingPool = await StakingPoolDAO.deploy({ arguments: [SNT.options.address, 100, 20, 10] }).send();
|
||||
const encodedCall = StakingPool.methods.stake("10000000000").encodeABI();
|
||||
|
||||
await web3.eth.sendTransaction({from: iuri, to: StakingPool.options.address, value: "100000000000000000"});
|
||||
|
||||
await SNT.methods.approveAndCall(StakingPool.options.address, "10000000000", encodedCall).send({from: iuri});
|
||||
await SNT.methods.approveAndCall(StakingPool.options.address, "10000000000", encodedCall).send({from: jonathan});
|
||||
await SNT.methods.approveAndCall(StakingPool.options.address, "10000000000", encodedCall).send({from: richard});
|
||||
await SNT.methods.approveAndCall(StakingPool.options.address, "10000000000", encodedCall).send({from: pascal});
|
||||
await SNT.methods.approveAndCall(StakingPool.options.address, "10000000000", encodedCall).send({from: michael});
|
||||
await SNT.methods.approveAndCall(StakingPool.options.address, "10000000000", encodedCall).send({from: eric});
|
||||
|
||||
// Mine 100 blocks
|
||||
for(let i = 0; i < 100; i++){
|
||||
await mineAtTimestamp(12345678);
|
||||
}
|
||||
})
|
||||
|
||||
describe("contract functionality", () => {
|
||||
it("contract should be owned by itself", async () => {
|
||||
const controller = await StakingPool.methods.controller().call();
|
||||
assert.strictEqual(controller, StakingPool.options.address);
|
||||
});
|
||||
});
|
||||
|
||||
describe("proposal creation", () => {
|
||||
it("non token holders can not submit proposals", async () => {
|
||||
const toSend = StakingPool.methods.addProposal(andre, 1, "0x", "0x");
|
||||
await assert.reverts(toSend, {from: andre}, "Returned error: VM Exception while processing transaction: revert Token balance is required to perform this operation");
|
||||
});
|
||||
|
||||
it("token holders can create proposals", async () => {
|
||||
const receipt = await StakingPool.methods.addProposal(richard, 1, "0x", "0x").send({from: richard});
|
||||
assert.eventEmitted(receipt, 'NewProposal');
|
||||
});
|
||||
});
|
||||
|
||||
describe("voting", () => {
|
||||
before(async () => {
|
||||
const balance = await StakingPool.methods.balanceOf(jonathan).call();
|
||||
await StakingPool.methods.transfer(eric, balance).send({from: jonathan});
|
||||
});
|
||||
|
||||
let proposalId;
|
||||
beforeEach(async () => {
|
||||
const receipt = await StakingPool.methods.addProposal(richard, 1, "0x", "0x").send({from: richard});
|
||||
proposalId = receipt.events.NewProposal.returnValues.proposalId;
|
||||
});
|
||||
|
||||
it("only those having balance can vote on a proposal", async () => {
|
||||
const toSend = StakingPool.methods.vote(proposalId, true);
|
||||
await assert.reverts(toSend, {from: jonathan}, "Returned error: VM Exception while processing transaction: revert Not enough tokens at the moment of proposal creation");
|
||||
await StakingPool.methods.vote(proposalId, true).send({from: eric});
|
||||
});
|
||||
|
||||
it("accounts can vote more than once and results are affected accordingly", async () => {
|
||||
let votesY = await StakingPool.methods.votes(proposalId, true).call();
|
||||
let votesN = await StakingPool.methods.votes(proposalId, true).call();
|
||||
let myVote = await StakingPool.methods.voteOf(richard, proposalId).call();
|
||||
|
||||
assert.strictEqual(votesY, "0");
|
||||
assert.strictEqual(votesN, "0");
|
||||
|
||||
let receipt = await StakingPool.methods.vote(proposalId, true).send({from: richard});
|
||||
|
||||
votesY = await StakingPool.methods.votes(proposalId, true).call();
|
||||
votesN = await StakingPool.methods.votes(proposalId, false).call();
|
||||
|
||||
|
||||
assert.strictEqual(votesY, "10000000000");
|
||||
assert.strictEqual(votesN, "0");
|
||||
|
||||
receipt = await StakingPool.methods.vote(proposalId, false).send({from: richard});
|
||||
|
||||
votesY = await StakingPool.methods.votes(proposalId, true).call();
|
||||
votesN = await StakingPool.methods.votes(proposalId, false).call();
|
||||
|
||||
assert.strictEqual(votesY, "0");
|
||||
assert.strictEqual(votesN, "10000000000");
|
||||
});
|
||||
|
||||
it("voting is only valid during the period it is active", async () => {
|
||||
const toSend = await StakingPool.methods.vote(proposalId, false);
|
||||
|
||||
await toSend.send({from: richard});
|
||||
|
||||
// Mine 20 blocks
|
||||
for(let i = 0; i < 20; i++){
|
||||
await mineAtTimestamp(12345678);
|
||||
}
|
||||
|
||||
await assert.reverts(toSend, {from: richard}, "Returned error: VM Exception while processing transaction: revert Proposal has already ended");
|
||||
});
|
||||
|
||||
it("check that vote result matches what was voted", async () => {
|
||||
await StakingPool.methods.vote(proposalId, true).send({from: eric});
|
||||
await StakingPool.methods.vote(proposalId, true).send({from: michael});
|
||||
await StakingPool.methods.vote(proposalId, false).send({from: pascal});
|
||||
await StakingPool.methods.vote(proposalId, false).send({from: richard});
|
||||
|
||||
// Mine 20 blocks
|
||||
for(let i = 0; i < 20; i++){
|
||||
await mineAtTimestamp(12345678);
|
||||
}
|
||||
|
||||
let votesY = await StakingPool.methods.votes(proposalId, true).call();
|
||||
let votesN = await StakingPool.methods.votes(proposalId, false).call();
|
||||
|
||||
const result = await StakingPool.methods.isProposalApproved(proposalId).call();
|
||||
|
||||
assert.strictEqual(votesY, "30000000000");
|
||||
assert.strictEqual(votesN, "20000000000");
|
||||
assert.strictEqual(result.approved, true);
|
||||
assert.strictEqual(result.executed, false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("proposal execution", () => {
|
||||
let proposalId;
|
||||
beforeEach(async () => {
|
||||
const receipt = await StakingPool.methods.addProposal("0x00000000000000000000000000000000000000AA", 12345, "0x", "0x").send({from: richard});
|
||||
proposalId = receipt.events.NewProposal.returnValues.proposalId;
|
||||
});
|
||||
|
||||
it("active voting proposals cant be executed", async () => {
|
||||
await StakingPool.methods.vote(proposalId, true).send({from: richard});
|
||||
const toSend = StakingPool.methods.executeTransaction(proposalId);
|
||||
await assert.reverts(toSend, {from: richard}, "Returned error: VM Exception while processing transaction: revert Voting is still active");
|
||||
});
|
||||
|
||||
it("unapproved proposals cant be executed", async () => {
|
||||
await StakingPool.methods.vote(proposalId, false).send({from: richard});
|
||||
|
||||
// Mine 20 blocks
|
||||
for(let i = 0; i < 20; i++){
|
||||
await mineAtTimestamp(12345678);
|
||||
}
|
||||
|
||||
const toSend = StakingPool.methods.executeTransaction(proposalId);
|
||||
await assert.reverts(toSend, {from: iuri}, "Returned error: VM Exception while processing transaction: revert Proposal wasn't approved");
|
||||
});
|
||||
|
||||
it("approved proposals can be executed", async () => {
|
||||
await StakingPool.methods.vote(proposalId, true).send({from: richard});
|
||||
|
||||
// Mine 20 blocks
|
||||
for(let i = 0; i < 20; i++){
|
||||
await mineAtTimestamp(12345678);
|
||||
}
|
||||
|
||||
let result = await StakingPool.methods.isProposalApproved(proposalId).call();
|
||||
assert.strictEqual(result.approved, true);
|
||||
assert.strictEqual(result.executed, false);
|
||||
|
||||
const receipt = await StakingPool.methods.executeTransaction(proposalId).send({from: iuri});
|
||||
|
||||
const destinationBalance = await web3.eth.getBalance("0x00000000000000000000000000000000000000AA");
|
||||
assert.strictEqual(destinationBalance, "12345");
|
||||
|
||||
result = await StakingPool.methods.isProposalApproved(proposalId).call();
|
||||
assert.strictEqual(result.executed, true);
|
||||
});
|
||||
|
||||
it("approved proposals can't be executed twice", async () => {
|
||||
await StakingPool.methods.vote(proposalId, true).send({from: richard});
|
||||
|
||||
// Mine 20 blocks
|
||||
for(let i = 0; i < 20; i++){
|
||||
await mineAtTimestamp(12345678);
|
||||
}
|
||||
|
||||
await StakingPool.methods.executeTransaction(proposalId).send({from: iuri});
|
||||
await assert.reverts(StakingPool.methods.executeTransaction(proposalId), {from: iuri}, "Returned error: VM Exception while processing transaction: revert Proposal already executed");
|
||||
});
|
||||
|
||||
it("approved proposals can't be executed after they expire", async () => {
|
||||
await StakingPool.methods.vote(proposalId, true).send({from: richard});
|
||||
// Mine 40 blocks
|
||||
for(let i = 0; i < 40; i++){
|
||||
await mineAtTimestamp(12345678);
|
||||
}
|
||||
await assert.reverts(StakingPool.methods.executeTransaction(proposalId), {from: iuri}, "Returned error: VM Exception while processing transaction: revert Proposal is already expired");
|
||||
});
|
||||
|
||||
|
||||
it("proposals can execute contract functions", async () => {
|
||||
const initialBalance = await SNT.methods.balanceOf("0xAA000000000000000000000000000000000000AA").call();
|
||||
assert.strictEqual(initialBalance, "0");
|
||||
|
||||
const encodedCall = SNT.methods.transfer("0xAA000000000000000000000000000000000000AA", "12345").encodeABI();
|
||||
const receipt = await StakingPool.methods.addProposal(SNT.options.address, 0, encodedCall, "0x").send({from: richard});
|
||||
proposalId = receipt.events.NewProposal.returnValues.proposalId;
|
||||
|
||||
await StakingPool.methods.vote(proposalId, true).send({from: richard});
|
||||
|
||||
// Mine 20 blocks
|
||||
for(let i = 0; i < 20; i++){
|
||||
await mineAtTimestamp(12345678);
|
||||
}
|
||||
|
||||
await StakingPool.methods.executeTransaction(proposalId).send({from: iuri});
|
||||
|
||||
const finalBalance = await SNT.methods.balanceOf("0xAA000000000000000000000000000000000000AA").call();
|
||||
assert.strictEqual(finalBalance, "12345");
|
||||
});
|
||||
})
|
||||
|
||||
});
|
Loading…
Reference in New Issue