From 02b5e2a00f2ca4179139e7c65dcdbddcedddaab5 Mon Sep 17 00:00:00 2001 From: Johann Barbie Date: Thu, 12 Dec 2019 14:21:10 +0100 Subject: [PATCH 1/6] basic register proposal done --- contracts/TokenGovernance.sol | 54 +++++++++++++++++++++++++++++++++++ test/tokenGovernance.js | 43 ++++++++++++++++++++++++++++ 2 files changed, 97 insertions(+) create mode 100644 contracts/TokenGovernance.sol create mode 100644 test/tokenGovernance.js diff --git a/contracts/TokenGovernance.sol b/contracts/TokenGovernance.sol new file mode 100644 index 0000000..be807e0 --- /dev/null +++ b/contracts/TokenGovernance.sol @@ -0,0 +1,54 @@ + +/** + * Copyright (c) 2018-present, Leap DAO (leapdao.org) + * + * This source code is licensed under the Mozilla Public License, version 2, + * found in the LICENSE file in the root directory of this source tree. + */ + +pragma solidity 0.5.2; + +import "../node_modules/openzeppelin-solidity/contracts/token/ERC20/IERC20.sol"; + +contract TokenGovernance { + + uint256 constant PROPOSAL_STAKE = 5000000000000000000000; + + struct Proposal { + address initiator; + uint256 openTime; + mapping(address => uint256) votes; + bool finalized; + } + + mapping(bytes32 => Proposal) public proposals; + address public mvgAddress; + address public leapAddr; + + event ProposalRegistered(bytes32 indexed ipfsHash, address indexed initiator); + event VoiceCasted(uint256 indexed ipfsHash, address indexed subject, uint256 weight); + event VoiceChallenged(uint256 indexed ipfsHash, address indexed subject, address challenger); + event ProposalFinalized(bytes32 indexed ipfsHash, bool isApproved); + + + constructor(address _mvgAddress, address _leapAddr) public { + mvgAddress = _mvgAddress; + leapAddr = _leapAddr; + } + + function registerProposal(bytes32 _proposalHash) public { + // make sure same proposals hasn't been opened before + require(proposals[_proposalHash].openTime == 0, "proposal already exists"); + + // get instance of token contract and pull proposal stake + IERC20 leapToken = IERC20(leapAddr); + leapToken.transferFrom(msg.sender, address(this), PROPOSAL_STAKE); + + // create a new proposal in storage + proposals[_proposalHash] = Proposal(msg.sender, now, false); + + // emit event for frontend + emit ProposalRegistered(_proposalHash, msg.sender); + } + +} \ No newline at end of file diff --git a/test/tokenGovernance.js b/test/tokenGovernance.js new file mode 100644 index 0000000..2fff0fe --- /dev/null +++ b/test/tokenGovernance.js @@ -0,0 +1,43 @@ +import chai from 'chai'; +import EVMRevert from './helpers/EVMRevert'; + +const NativeToken = artifacts.require('NativeToken'); +const TokenGovernance = artifacts.require('TokenGovernance'); + +chai.use(require('chai-as-promised')).should(); + +contract('TokenGovernance', (accounts) => { + + let gov; + let leapToken; + const proposalHash = '0x1122334411223344112233441122334411223344112233441122334411223344'; + const proposalStake = '5000000000000000000000'; + + beforeEach(async () => { + leapToken = await NativeToken.new("Leap Token", "Leap", 18); + leapToken.mint(accounts[0], proposalStake); + gov = await TokenGovernance.new(accounts[0], leapToken.address); + }); + + + it('should fail if funds not approved', async () => { + // register proposal + await gov.registerProposal(proposalHash).should.be.rejectedWith(EVMRevert); + }); + + it('should allow to create proposal', async () => { + // allow gov contract to pull funds + leapToken.approve(gov.address, proposalStake); + // register proposal + await gov.registerProposal(proposalHash); + // read proposal + const rsp = await gov.proposals(proposalHash); + assert(rsp.openTime > 0); + + // check that same proposal can not be rigestered twice + leapToken.mint(accounts[0], proposalStake); + leapToken.approve(gov.address, proposalStake); + await gov.registerProposal(proposalHash).should.be.rejectedWith(EVMRevert); + }); + +}); From 6f9c6bbbce7a40556f2069ef5c196fed4710a153 Mon Sep 17 00:00:00 2001 From: Johann Barbie Date: Fri, 13 Dec 2019 23:57:30 +0100 Subject: [PATCH 2/6] first vote --- contracts/TokenGovernance.sol | 127 +++++++++++++++++++++++++++++----- test/tokenGovernance.js | 49 +++++++++++-- 2 files changed, 155 insertions(+), 21 deletions(-) diff --git a/contracts/TokenGovernance.sol b/contracts/TokenGovernance.sol index be807e0..42a42f9 100644 --- a/contracts/TokenGovernance.sol +++ b/contracts/TokenGovernance.sol @@ -9,46 +9,141 @@ pragma solidity 0.5.2; import "../node_modules/openzeppelin-solidity/contracts/token/ERC20/IERC20.sol"; +import "./Bridge.sol"; +import "./Vault.sol"; +import "./TxLib.sol"; contract TokenGovernance { uint256 constant PROPOSAL_STAKE = 5000000000000000000000; + uint32 constant PROPOSAL_TIME = 604800; // 60 * 60 * 24 * 7 = 7 days + address public mvgAddress; + IERC20 public leapToken; + Bridge public bridge; + Vault public vault; struct Proposal { address initiator; - uint256 openTime; - mapping(address => uint256) votes; + uint32 openTime; bool finalized; + uint256 yesVotes; + uint256 noVotes; + mapping(address => int256) votes; + mapping(bytes32 => address) usedTxns; } mapping(bytes32 => Proposal) public proposals; - address public mvgAddress; - address public leapAddr; - - event ProposalRegistered(bytes32 indexed ipfsHash, address indexed initiator); - event VoiceCasted(uint256 indexed ipfsHash, address indexed subject, uint256 weight); - event VoiceChallenged(uint256 indexed ipfsHash, address indexed subject, address challenger); - event ProposalFinalized(bytes32 indexed ipfsHash, bool isApproved); + event ProposalRegistered(bytes32 indexed proposalHash, address indexed initiator); + event VoiceCasted(bytes32 indexed proposalHash, address indexed subject, uint256 weight); + event VoiceChallenged(bytes32 indexed proposalHash, address indexed subject, address challenger); + event ProposalFinalized(bytes32 indexed proposalHash, bool isApproved); - constructor(address _mvgAddress, address _leapAddr) public { + constructor(address _mvgAddress, IERC20 _leapToken, Vault _vault) public { mvgAddress = _mvgAddress; - leapAddr = _leapAddr; + leapToken = _leapToken; + vault = _vault; + bridge = _vault.bridge(); + } + + function _revertVote(Proposal storage proposal, bytes32 txHash) internal { + address signer = proposal.usedTxns[txHash]; + delete proposal.usedTxns[txHash]; + int256 votes = proposal.votes[signer]; + delete proposal.votes[signer]; + if (votes > 0) { + proposal.yesVotes -= uint256(votes); + } else { + proposal.noVotes -= uint256(votes * -1); + } } function registerProposal(bytes32 _proposalHash) public { // make sure same proposals hasn't been opened before require(proposals[_proposalHash].openTime == 0, "proposal already exists"); - // get instance of token contract and pull proposal stake - IERC20 leapToken = IERC20(leapAddr); leapToken.transferFrom(msg.sender, address(this), PROPOSAL_STAKE); - // create a new proposal in storage - proposals[_proposalHash] = Proposal(msg.sender, now, false); - + proposals[_proposalHash] = Proposal(msg.sender, uint32(now), false, 0, 0); // emit event for frontend emit ProposalRegistered(_proposalHash, msg.sender); } + function castVote(bytes32 _proposalHash, bytes32[] memory _proof, uint8 _outputIndex, bool isYes) public { + Proposal memory proposal = proposals[_proposalHash]; + require(proposal.openTime > 0, "proposal does not exist"); + require(proposal.finalized == false, "proposal already finalized"); + uint32 timestamp; + (, timestamp,,) = bridge.periods(_proof[0]); + require(timestamp > 0, "The referenced period was not submitted to bridge"); + require(timestamp <= proposal.openTime, "The transaction was submitted after the vote open time"); + + bytes memory txData; + bytes32 txHash; + (, txHash, txData) = TxLib.validateProof(96, _proof); + + // parse tx and check if it is usable for voting + TxLib.Tx memory tx = TxLib.parseTx(txData); + TxLib.Output memory out = tx.outs[_outputIndex]; + require(out.value > 0, "UTXO has no value"); + require(out.owner == msg.sender, "msg.sender not owner of utxo"); + // TODO: fix + require(address(leapToken) == vault.getTokenAddr(out.color), "not Leap UTXO"); + + if (proposals[_proposalHash].votes[msg.sender] != 0) { + // TODO: clean up previous vote + _revertVote(proposals[_proposalHash], txHash); + } + if (isYes) { + proposals[_proposalHash].yesVotes += out.value; + proposals[_proposalHash].votes[msg.sender] = int256(out.value); + } else { + proposals[_proposalHash].noVotes += out.value; + proposals[_proposalHash].votes[msg.sender] = int256(out.value) * -1; + } + proposals[_proposalHash].usedTxns[txHash] = msg.sender; + emit VoiceCasted(_proposalHash, msg.sender, out.value); + } + + function challengeUTXO(bytes32 _proposalHash, bytes32[] memory _proof, uint8 _inputIndex, address _signer) public { + uint32 timestamp; + (, timestamp,,) = bridge.periods(_proof[0]); + require(timestamp > 0, "The referenced period was not submitted to bridge"); + require(timestamp <= proposals[_proposalHash].openTime, "The transaction was submitted after the vote open time"); + + bytes memory txData; + bytes32 txHash; + (, txHash, txData) = TxLib.validateProof(96, _proof); + + // parse tx + TxLib.Tx memory txn = TxLib.parseTx(txData); + // checking that the transactions has been used in a vote + address signer = proposals[_proposalHash].usedTxns[txn.ins[_inputIndex].outpoint.hash]; + // note: we don't check the output index here, assume that each transactions is only used for 1 vote + require(signer != address(0), "prevout check failed on hash"); + + // substract vote + _revertVote(proposals[_proposalHash], txHash); + emit VoiceChallenged(_proposalHash, signer, msg.sender); + } + + function finalizeProposal(bytes32 _proposalHash) public { + Proposal memory proposal = proposals[_proposalHash]; + require(proposal.finalized == false, "already finalized"); + require(proposal.openTime > 0, "proposal does not exist"); + require(proposal.openTime + PROPOSAL_TIME < uint32(now), "proposal time not exceeded"); + // can't delete full mappings + // delete proposals[_proposalHash].votes; + // delete proposals[_proposalHash].usedTxns; + proposals[_proposalHash].finalized = true; + // return stake + if (proposal.yesVotes > proposal.noVotes) { + leapToken.transfer(proposal.initiator, PROPOSAL_STAKE); + emit ProposalFinalized(_proposalHash, true); + } else { + leapToken.transfer(mvgAddress, PROPOSAL_STAKE); + emit ProposalFinalized(_proposalHash, false); + } + } + } \ No newline at end of file diff --git a/test/tokenGovernance.js b/test/tokenGovernance.js index 2fff0fe..e2caed7 100644 --- a/test/tokenGovernance.js +++ b/test/tokenGovernance.js @@ -1,22 +1,51 @@ import chai from 'chai'; -import EVMRevert from './helpers/EVMRevert'; +import { Tx } from 'leap-core'; +import { EVMRevert, submitNewPeriodWithTx } from './helpers'; +const AdminableProxy = artifacts.require('AdminableProxy'); const NativeToken = artifacts.require('NativeToken'); +const Bridge = artifacts.require('Bridge'); +const Vault = artifacts.require('Vault'); const TokenGovernance = artifacts.require('TokenGovernance'); chai.use(require('chai-as-promised')).should(); contract('TokenGovernance', (accounts) => { - + let bridge; + let vault; let gov; let leapToken; + let proxy; const proposalHash = '0x1122334411223344112233441122334411223344112233441122334411223344'; const proposalStake = '5000000000000000000000'; + const parentBlockInterval = 0; + const alice = accounts[0]; + const bob = accounts[1]; + + const submitNewPeriod = txs => submitNewPeriodWithTx(txs, bridge, { from: bob }); - beforeEach(async () => { + before(async () => { leapToken = await NativeToken.new("Leap Token", "Leap", 18); leapToken.mint(accounts[0], proposalStake); - gov = await TokenGovernance.new(accounts[0], leapToken.address); + + const bridgeCont = await Bridge.new(); + let data = await bridgeCont.contract.methods.initialize(parentBlockInterval).encodeABI(); + proxy = await AdminableProxy.new(bridgeCont.address, data, {from: accounts[2]}); + bridge = await Bridge.at(proxy.address); + + data = await bridge.contract.methods.setOperator(bob).encodeABI(); + await proxy.applyProposal(data, {from: accounts[2]}).should.be.fulfilled; + + const vaultCont = await Vault.new(); + data = await vaultCont.contract.methods.initialize(bridge.address).encodeABI(); + proxy = await AdminableProxy.new(vaultCont.address, data, {from: accounts[2]}); + vault = await Vault.at(proxy.address); + + // register first token + data = await vault.contract.methods.registerToken(leapToken.address, 0).encodeABI(); + await proxy.applyProposal(data, {from: accounts[2]}).should.be.fulfilled; + + gov = await TokenGovernance.new(accounts[0], leapToken.address, vault.address); }); @@ -26,18 +55,28 @@ contract('TokenGovernance', (accounts) => { }); it('should allow to create proposal', async () => { + const amount = 34567000000; + const depositTx = Tx.deposit(123, amount, alice); + + const period1 = await submitNewPeriod([depositTx]); + const inputProof = period1.proof(depositTx); + // allow gov contract to pull funds leapToken.approve(gov.address, proposalStake); // register proposal await gov.registerProposal(proposalHash); // read proposal - const rsp = await gov.proposals(proposalHash); + let rsp = await gov.proposals(proposalHash); assert(rsp.openTime > 0); // check that same proposal can not be rigestered twice leapToken.mint(accounts[0], proposalStake); leapToken.approve(gov.address, proposalStake); await gov.registerProposal(proposalHash).should.be.rejectedWith(EVMRevert); + + await gov.castVote(proposalHash, inputProof, 0, true); + rsp = await gov.proposals(proposalHash); + assert.equal(rsp.yesVotes, amount); }); }); From 183c1c3310c77bfdd3af70e20e092fc9d3339c3c Mon Sep 17 00:00:00 2001 From: Johann Barbie Date: Sat, 14 Dec 2019 08:41:14 +0100 Subject: [PATCH 3/6] complete happy flow --- contracts/TokenGovernance.sol | 3 ++- test/tokenGovernance.js | 27 +++++++++++++++++++++++++-- 2 files changed, 27 insertions(+), 3 deletions(-) diff --git a/contracts/TokenGovernance.sol b/contracts/TokenGovernance.sol index 42a42f9..b879375 100644 --- a/contracts/TokenGovernance.sol +++ b/contracts/TokenGovernance.sol @@ -131,7 +131,8 @@ contract TokenGovernance { Proposal memory proposal = proposals[_proposalHash]; require(proposal.finalized == false, "already finalized"); require(proposal.openTime > 0, "proposal does not exist"); - require(proposal.openTime + PROPOSAL_TIME < uint32(now), "proposal time not exceeded"); + // disable to simplify testing + // require(proposal.openTime + PROPOSAL_TIME < uint32(now), "proposal time not exceeded"); // can't delete full mappings // delete proposals[_proposalHash].votes; // delete proposals[_proposalHash].usedTxns; diff --git a/test/tokenGovernance.js b/test/tokenGovernance.js index e2caed7..2e479d7 100644 --- a/test/tokenGovernance.js +++ b/test/tokenGovernance.js @@ -18,6 +18,7 @@ contract('TokenGovernance', (accounts) => { let proxy; const proposalHash = '0x1122334411223344112233441122334411223344112233441122334411223344'; const proposalStake = '5000000000000000000000'; + const totalSupply = '10000000000000000000000'; const parentBlockInterval = 0; const alice = accounts[0]; const bob = accounts[1]; @@ -26,7 +27,7 @@ contract('TokenGovernance', (accounts) => { before(async () => { leapToken = await NativeToken.new("Leap Token", "Leap", 18); - leapToken.mint(accounts[0], proposalStake); + leapToken.mint(accounts[0], totalSupply); const bridgeCont = await Bridge.new(); let data = await bridgeCont.contract.methods.initialize(parentBlockInterval).encodeABI(); @@ -63,20 +64,42 @@ contract('TokenGovernance', (accounts) => { // allow gov contract to pull funds leapToken.approve(gov.address, proposalStake); + const balBefore = await leapToken.balanceOf(alice); // register proposal await gov.registerProposal(proposalHash); + let bal = await leapToken.balanceOf(gov.address); + assert.equal(bal, proposalStake); // read proposal let rsp = await gov.proposals(proposalHash); assert(rsp.openTime > 0); // check that same proposal can not be rigestered twice - leapToken.mint(accounts[0], proposalStake); leapToken.approve(gov.address, proposalStake); await gov.registerProposal(proposalHash).should.be.rejectedWith(EVMRevert); + // cast vote + await gov.castVote(proposalHash, inputProof, 0, false); + rsp = await gov.proposals(proposalHash); + assert.equal(rsp.noVotes, amount); + + // recast vote await gov.castVote(proposalHash, inputProof, 0, true); rsp = await gov.proposals(proposalHash); assert.equal(rsp.yesVotes, amount); + assert.equal(rsp.noVotes, 0); + + // finalize and count + await gov.finalizeProposal(proposalHash); + rsp = await gov.proposals(proposalHash); + assert.equal(rsp.yesVotes, amount); + assert.equal(rsp.noVotes, 0); + assert.equal(rsp.finalized, true); + // token governance should have returned the stake + bal = await leapToken.balanceOf(gov.address); + assert.equal(bal, 0); + // alice should have same amount like before opening the vote + bal = await leapToken.balanceOf(alice); + assert.equal(bal.toString(), balBefore.toString()); }); }); From 57974489e73f43fa0e7c7f37880044e63d0b41c5 Mon Sep 17 00:00:00 2001 From: Johann Barbie Date: Sat, 14 Dec 2019 09:27:19 +0100 Subject: [PATCH 4/6] done --- contracts/TokenGovernance.sol | 11 ++-- test/tokenGovernance.js | 121 +++++++++++++++++++++++++++++++--- 2 files changed, 117 insertions(+), 15 deletions(-) diff --git a/contracts/TokenGovernance.sol b/contracts/TokenGovernance.sol index b879375..0d2285f 100644 --- a/contracts/TokenGovernance.sol +++ b/contracts/TokenGovernance.sol @@ -35,7 +35,7 @@ contract TokenGovernance { mapping(bytes32 => Proposal) public proposals; event ProposalRegistered(bytes32 indexed proposalHash, address indexed initiator); - event VoiceCasted(bytes32 indexed proposalHash, address indexed subject, uint256 weight); + event VoiceCasted(bytes32 indexed proposalHash, address indexed subject, uint256 weight, bytes32 txHash); event VoiceChallenged(bytes32 indexed proposalHash, address indexed subject, address challenger); event ProposalFinalized(bytes32 indexed proposalHash, bool isApproved); @@ -102,10 +102,10 @@ contract TokenGovernance { proposals[_proposalHash].votes[msg.sender] = int256(out.value) * -1; } proposals[_proposalHash].usedTxns[txHash] = msg.sender; - emit VoiceCasted(_proposalHash, msg.sender, out.value); + emit VoiceCasted(_proposalHash, msg.sender, out.value, txHash); } - function challengeUTXO(bytes32 _proposalHash, bytes32[] memory _proof, uint8 _inputIndex, address _signer) public { + function challengeUTXO(bytes32 _proposalHash, bytes32[] memory _proof, uint8 _inputIndex) public { uint32 timestamp; (, timestamp,,) = bridge.periods(_proof[0]); require(timestamp > 0, "The referenced period was not submitted to bridge"); @@ -113,8 +113,7 @@ contract TokenGovernance { bytes memory txData; bytes32 txHash; - (, txHash, txData) = TxLib.validateProof(96, _proof); - + (, txHash, txData) = TxLib.validateProof(64, _proof); // parse tx TxLib.Tx memory txn = TxLib.parseTx(txData); // checking that the transactions has been used in a vote @@ -123,7 +122,7 @@ contract TokenGovernance { require(signer != address(0), "prevout check failed on hash"); // substract vote - _revertVote(proposals[_proposalHash], txHash); + _revertVote(proposals[_proposalHash], txn.ins[_inputIndex].outpoint.hash); emit VoiceChallenged(_proposalHash, signer, msg.sender); } diff --git a/test/tokenGovernance.js b/test/tokenGovernance.js index 2e479d7..964b854 100644 --- a/test/tokenGovernance.js +++ b/test/tokenGovernance.js @@ -1,7 +1,10 @@ import chai from 'chai'; -import { Tx } from 'leap-core'; +import { Tx, Input, Output, Outpoint } from 'leap-core'; import { EVMRevert, submitNewPeriodWithTx } from './helpers'; +const time = require('./helpers/time'); +require('./helpers/setup'); + const AdminableProxy = artifacts.require('AdminableProxy'); const NativeToken = artifacts.require('NativeToken'); const Bridge = artifacts.require('Bridge'); @@ -18,10 +21,13 @@ contract('TokenGovernance', (accounts) => { let proxy; const proposalHash = '0x1122334411223344112233441122334411223344112233441122334411223344'; const proposalStake = '5000000000000000000000'; - const totalSupply = '10000000000000000000000'; + const totalSupply = '20000000000000000000000'; const parentBlockInterval = 0; const alice = accounts[0]; + const alicePriv = '0x278a5de700e29faae8e40e366ec5012b5ec63d36ec77e8a2417154cc1d25383f'; const bob = accounts[1]; + const mvgAddress = accounts[2]; + const voiceAmount = 34567000000; const submitNewPeriod = txs => submitNewPeriodWithTx(txs, bridge, { from: bob }); @@ -46,7 +52,7 @@ contract('TokenGovernance', (accounts) => { data = await vault.contract.methods.registerToken(leapToken.address, 0).encodeABI(); await proxy.applyProposal(data, {from: accounts[2]}).should.be.fulfilled; - gov = await TokenGovernance.new(accounts[0], leapToken.address, vault.address); + gov = await TokenGovernance.new(mvgAddress, leapToken.address, vault.address); }); @@ -55,9 +61,9 @@ contract('TokenGovernance', (accounts) => { await gov.registerProposal(proposalHash).should.be.rejectedWith(EVMRevert); }); - it('should allow to create proposal', async () => { - const amount = 34567000000; - const depositTx = Tx.deposit(123, amount, alice); + it('should allow to create, vote and finalize proposal', async () => { + + const depositTx = Tx.deposit(123, voiceAmount, alice); const period1 = await submitNewPeriod([depositTx]); const inputProof = period1.proof(depositTx); @@ -80,18 +86,18 @@ contract('TokenGovernance', (accounts) => { // cast vote await gov.castVote(proposalHash, inputProof, 0, false); rsp = await gov.proposals(proposalHash); - assert.equal(rsp.noVotes, amount); + assert.equal(rsp.noVotes, voiceAmount); // recast vote await gov.castVote(proposalHash, inputProof, 0, true); rsp = await gov.proposals(proposalHash); - assert.equal(rsp.yesVotes, amount); + assert.equal(rsp.yesVotes, voiceAmount); assert.equal(rsp.noVotes, 0); // finalize and count await gov.finalizeProposal(proposalHash); rsp = await gov.proposals(proposalHash); - assert.equal(rsp.yesVotes, amount); + assert.equal(rsp.yesVotes, voiceAmount); assert.equal(rsp.noVotes, 0); assert.equal(rsp.finalized, true); // token governance should have returned the stake @@ -100,6 +106,103 @@ contract('TokenGovernance', (accounts) => { // alice should have same amount like before opening the vote bal = await leapToken.balanceOf(alice); assert.equal(bal.toString(), balBefore.toString()); + + // try sending vote to finalized proposal + await gov.castVote(proposalHash, inputProof, 0, false).should.be.rejectedWith(EVMRevert); + // try to re-open same vote + await gov.registerProposal(proposalHash).should.be.rejectedWith(EVMRevert); + }); + + it('should allow to fail proposal', async () => { + const anotherProposalHash = '0x6677889966778899667788996677889966778899667788996677889966778899'; + const transferTx = Tx.transfer( + [new Input(new Outpoint(anotherProposalHash, 0))], + [new Output(voiceAmount, alice)] + ).sign([alicePriv]); + + const period1 = await submitNewPeriod([transferTx]); + const inputProof = period1.proof(transferTx); + + // allow gov contract to pull funds + leapToken.approve(gov.address, proposalStake); + // register proposal + await gov.registerProposal(anotherProposalHash); + let bal = await leapToken.balanceOf(gov.address); + assert.equal(bal, proposalStake); + + // cast vote + await gov.castVote(anotherProposalHash, inputProof, 0, false); + let rsp = await gov.proposals(anotherProposalHash); + assert.equal(rsp.noVotes, voiceAmount); + + // finalize and count + await gov.finalizeProposal(anotherProposalHash); + rsp = await gov.proposals(anotherProposalHash); + assert.equal(rsp.noVotes, voiceAmount); + assert.equal(rsp.finalized, true); + + // token governance should have returned the stake + bal = await leapToken.balanceOf(gov.address); + assert.equal(bal, 0); + // stake should now be in minimalViableGovernance, as vote resulted in NO + bal = await leapToken.balanceOf(mvgAddress); + assert.equal(bal.toString(), proposalStake); + }); + + it('should prevent vote with younger UTXO', async () => { + const anotherProposalHash = '0x5500aabb5500aabb5500aabb5500aabb5500aabb5500aabb5500aabb5500aabb'; + + // allow gov contract to pull funds + leapToken.approve(gov.address, proposalStake); + // register proposal + await gov.registerProposal(anotherProposalHash); + let bal = await leapToken.balanceOf(gov.address); + assert.equal(bal, proposalStake); + + // increase time + await time.increaseTo((await time.latest()) + 1); + // create utxo after vote start time + const transferTx = Tx.transfer( + [new Input(new Outpoint(anotherProposalHash, 0))], + [new Output(voiceAmount, alice)] + ).sign([alicePriv]); + // submit period + const period1 = await submitNewPeriod([transferTx]); + const inputProof = period1.proof(transferTx); + + // try to cast vote + await gov.castVote(anotherProposalHash, inputProof, 0, true).should.be.rejectedWith(EVMRevert); + }); + + it('should allow challenge UTXO', async () => { + const anotherProposalHash = '0xccddeeffccddeeffccddeeffccddeeffccddeeffccddeeffccddeeffccddeeff'; + // prepare 2 transactions + const depositTx = Tx.deposit(123, voiceAmount, alice); + const transferTx = Tx.transfer( + [new Input(new Outpoint(depositTx.hash(), 0))], + [new Output(voiceAmount, bob)] + ).sign([alicePriv]); + // submit and create proofs + const period1 = await submitNewPeriod([depositTx, transferTx]); + const inputProofA = period1.proof(depositTx); + const inputProofB = period1.proof(transferTx); + + // allow gov contract to pull funds + leapToken.approve(gov.address, proposalStake); + // register proposal + await gov.registerProposal(anotherProposalHash); + + // cast vote with spent UTXO + await gov.castVote(anotherProposalHash, inputProofA, 0, true); + let rsp = await gov.proposals(anotherProposalHash); + assert.equal(rsp.yesVotes, voiceAmount); + + // challenge vote + await gov.challengeUTXO(anotherProposalHash, inputProofB, 0); + + // check that vote got reverted + rsp = await gov.proposals(anotherProposalHash); + assert.equal(rsp.yesVotes, 0); }); }); From 8fb24bd76e46a7cbbce477b7102d86d6ec67b0c6 Mon Sep 17 00:00:00 2001 From: Johann Barbie Date: Mon, 16 Dec 2019 11:09:29 +0100 Subject: [PATCH 5/6] Update test/tokenGovernance.js Co-Authored-By: Maksim Daunarovich --- test/tokenGovernance.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/tokenGovernance.js b/test/tokenGovernance.js index 964b854..ff59cc7 100644 --- a/test/tokenGovernance.js +++ b/test/tokenGovernance.js @@ -79,7 +79,7 @@ contract('TokenGovernance', (accounts) => { let rsp = await gov.proposals(proposalHash); assert(rsp.openTime > 0); - // check that same proposal can not be rigestered twice + // check that same proposal can not be registered twice leapToken.approve(gov.address, proposalStake); await gov.registerProposal(proposalHash).should.be.rejectedWith(EVMRevert); From 5189d83eb3caa580065614ea0f50ddef907bece2 Mon Sep 17 00:00:00 2001 From: Johann Barbie Date: Tue, 17 Dec 2019 10:34:54 +0100 Subject: [PATCH 6/6] addressed review --- contracts/TokenGovernance.sol | 20 ++++++++++---------- test/tokenGovernance.js | 10 ++++++---- 2 files changed, 16 insertions(+), 14 deletions(-) diff --git a/contracts/TokenGovernance.sol b/contracts/TokenGovernance.sol index 0d2285f..273d34d 100644 --- a/contracts/TokenGovernance.sol +++ b/contracts/TokenGovernance.sol @@ -9,13 +9,16 @@ pragma solidity 0.5.2; import "../node_modules/openzeppelin-solidity/contracts/token/ERC20/IERC20.sol"; +import "../node_modules/openzeppelin-solidity/contracts/math/SafeMath.sol"; import "./Bridge.sol"; import "./Vault.sol"; import "./TxLib.sol"; contract TokenGovernance { + using SafeMath for uint256; uint256 constant PROPOSAL_STAKE = 5000000000000000000000; + uint256 constant DUST_THRESHOLD = 1000000000000000000; uint32 constant PROPOSAL_TIME = 604800; // 60 * 60 * 24 * 7 = 7 days address public mvgAddress; IERC20 public leapToken; @@ -52,9 +55,9 @@ contract TokenGovernance { int256 votes = proposal.votes[signer]; delete proposal.votes[signer]; if (votes > 0) { - proposal.yesVotes -= uint256(votes); + proposal.yesVotes = proposal.yesVotes.sub(uint256(votes)); } else { - proposal.noVotes -= uint256(votes * -1); + proposal.noVotes = proposal.noVotes.sub(uint256(votes * -1)); } } @@ -85,20 +88,18 @@ contract TokenGovernance { // parse tx and check if it is usable for voting TxLib.Tx memory tx = TxLib.parseTx(txData); TxLib.Output memory out = tx.outs[_outputIndex]; - require(out.value > 0, "UTXO has no value"); + require(out.value >= DUST_THRESHOLD, "UTXO is below dust threshold"); require(out.owner == msg.sender, "msg.sender not owner of utxo"); - // TODO: fix require(address(leapToken) == vault.getTokenAddr(out.color), "not Leap UTXO"); if (proposals[_proposalHash].votes[msg.sender] != 0) { - // TODO: clean up previous vote _revertVote(proposals[_proposalHash], txHash); } if (isYes) { - proposals[_proposalHash].yesVotes += out.value; + proposals[_proposalHash].yesVotes = proposal.yesVotes.add(out.value); proposals[_proposalHash].votes[msg.sender] = int256(out.value); } else { - proposals[_proposalHash].noVotes += out.value; + proposals[_proposalHash].noVotes = proposal.noVotes.add(out.value); proposals[_proposalHash].votes[msg.sender] = int256(out.value) * -1; } proposals[_proposalHash].usedTxns[txHash] = msg.sender; @@ -112,8 +113,7 @@ contract TokenGovernance { require(timestamp <= proposals[_proposalHash].openTime, "The transaction was submitted after the vote open time"); bytes memory txData; - bytes32 txHash; - (, txHash, txData) = TxLib.validateProof(64, _proof); + (,, txData) = TxLib.validateProof(64, _proof); // parse tx TxLib.Tx memory txn = TxLib.parseTx(txData); // checking that the transactions has been used in a vote @@ -131,7 +131,7 @@ contract TokenGovernance { require(proposal.finalized == false, "already finalized"); require(proposal.openTime > 0, "proposal does not exist"); // disable to simplify testing - // require(proposal.openTime + PROPOSAL_TIME < uint32(now), "proposal time not exceeded"); + require(proposal.openTime + PROPOSAL_TIME < uint32(now), "proposal time not exceeded"); // can't delete full mappings // delete proposals[_proposalHash].votes; // delete proposals[_proposalHash].usedTxns; diff --git a/test/tokenGovernance.js b/test/tokenGovernance.js index 964b854..4590e85 100644 --- a/test/tokenGovernance.js +++ b/test/tokenGovernance.js @@ -27,7 +27,7 @@ contract('TokenGovernance', (accounts) => { const alicePriv = '0x278a5de700e29faae8e40e366ec5012b5ec63d36ec77e8a2417154cc1d25383f'; const bob = accounts[1]; const mvgAddress = accounts[2]; - const voiceAmount = 34567000000; + const voiceAmount = '2000000000000000000'; const submitNewPeriod = txs => submitNewPeriodWithTx(txs, bridge, { from: bob }); @@ -94,7 +94,8 @@ contract('TokenGovernance', (accounts) => { assert.equal(rsp.yesVotes, voiceAmount); assert.equal(rsp.noVotes, 0); - // finalize and count + // wait, finalize and count + await time.increaseTo((await time.latest()) + 60 * 60 * 24 * 7 + 1); await gov.finalizeProposal(proposalHash); rsp = await gov.proposals(proposalHash); assert.equal(rsp.yesVotes, voiceAmount); @@ -135,7 +136,8 @@ contract('TokenGovernance', (accounts) => { let rsp = await gov.proposals(anotherProposalHash); assert.equal(rsp.noVotes, voiceAmount); - // finalize and count + // wait, finalize and count + await time.increaseTo((await time.latest()) + 60 * 60 * 24 * 7 + 1); await gov.finalizeProposal(anotherProposalHash); rsp = await gov.proposals(anotherProposalHash); assert.equal(rsp.noVotes, voiceAmount); @@ -156,7 +158,7 @@ contract('TokenGovernance', (accounts) => { leapToken.approve(gov.address, proposalStake); // register proposal await gov.registerProposal(anotherProposalHash); - let bal = await leapToken.balanceOf(gov.address); + const bal = await leapToken.balanceOf(gov.address); assert.equal(bal, proposalStake); // increase time