diff --git a/contracts/TestDAO.sol b/contracts/TestDAO.sol new file mode 100644 index 0000000..99f34ea --- /dev/null +++ b/contracts/TestDAO.sol @@ -0,0 +1,27 @@ +/* + + << Test DAO (since we need a token that we can send around in tests) >> + +*/ + +pragma solidity ^0.4.18; + +import "./dao/DelegatedShareholderAssociation.sol"; + +/** + * @title TestDAO + * @author Project Wyvern Developers + * + * + */ +contract TestDAO is DelegatedShareholderAssociation { + + string public constant name = "Test DAO"; + + function TestDAO (ERC20 sharesAddress, uint minimumSharesToPassAVote, uint minutesForDebate) public { + sharesTokenAddress = sharesAddress; + minimumQuorum = minimumSharesToPassAVote; + debatingPeriodInMinutes = minutesForDebate; + } + +} diff --git a/contracts/TestToken.sol b/contracts/TestToken.sol new file mode 100644 index 0000000..0101b4f --- /dev/null +++ b/contracts/TestToken.sol @@ -0,0 +1,33 @@ +/* + + << Test Token (for use with the Test DAO) >> + +*/ + +pragma solidity ^0.4.18; + +import "zeppelin-solidity/contracts/token/StandardToken.sol"; + +/** + * @title TestToken + * @author Project Wyvern Developers + * + * + */ +contract TestToken is StandardToken { + + uint constant public decimals = 18; + string constant public name = "Test Token"; + string constant public symbol = "TST"; + + uint constant public MINT_AMOUNT = 20000000 * (10 ** decimals); + + /** + * @dev Initialize the test token + */ + function TestToken () public { + balances[msg.sender] = MINT_AMOUNT; + totalSupply = MINT_AMOUNT; + } + +} diff --git a/contracts/WyvernDAO.sol b/contracts/WyvernDAO.sol index 4ee8f87..105da76 100644 --- a/contracts/WyvernDAO.sol +++ b/contracts/WyvernDAO.sol @@ -8,6 +8,12 @@ pragma solidity ^0.4.18; import "./dao/DelegatedShareholderAssociation.sol"; +/** + * @title WyvernDAO + * @author Project Wyvern Developers + * + * + */ contract WyvernDAO is DelegatedShareholderAssociation { string public constant name = "Project Wyvern DAO"; diff --git a/contracts/dao/DelegatedShareholderAssociation.sol b/contracts/dao/DelegatedShareholderAssociation.sol index 519d4c2..1bcc59d 100644 --- a/contracts/dao/DelegatedShareholderAssociation.sol +++ b/contracts/dao/DelegatedShareholderAssociation.sol @@ -49,7 +49,7 @@ contract DelegatedShareholderAssociation is TokenRecipient { mapping (address => uint) public delegatedAmountsByDelegate; uint public totalLockedTokens; - event ProposalAdded(uint proposalID, address recipient, uint amount, string description); + event ProposalAdded(uint proposalID, address recipient, uint amount, bytes metadataHash); event Voted(uint proposalID, bool position, address voter); event ProposalTallied(uint proposalID, uint result, uint quorum, bool active); event ChangeOfRules(uint newMinimumQuorum, uint newDebatingPeriodInMinutes, address newSharesTokenAddress); @@ -60,7 +60,7 @@ contract DelegatedShareholderAssociation is TokenRecipient { struct Proposal { address recipient; uint amount; - string description; + bytes metadataHash; uint votingDeadline; bool executed; bool proposalPassed; @@ -77,7 +77,7 @@ contract DelegatedShareholderAssociation is TokenRecipient { /* Only shareholders can execute a function with this modifier. */ modifier onlyShareholders { - require(sharesTokenAddress.balanceOf(msg.sender) > 0); + require(ERC20(sharesTokenAddress).balanceOf(msg.sender) > 0); _; } @@ -105,7 +105,7 @@ contract DelegatedShareholderAssociation is TokenRecipient { * @param tokensToLock number of tokens to be locked (sending address must have at least this many tokens) * @param delegate the address to which votes equal to the number of tokens locked will be delegated */ - function setDelegateAndLockTokens(uint tokensToLock, address delegate) onlyUndelegated public { + function setDelegateAndLockTokens(uint tokensToLock, address delegate) public onlyShareholders onlyUndelegated { require(ERC20(sharesTokenAddress).transferFrom(msg.sender, address(this), tokensToLock)); lockedDelegatingTokens[msg.sender] = tokensToLock; delegatedAmountsByDelegate[delegate] = tokensToLock; @@ -136,7 +136,7 @@ contract DelegatedShareholderAssociation is TokenRecipient { * @param minimumSharesToPassAVote proposal can vote only if the sum of shares held by all voters exceed this number * @param minutesForDebate the minimum amount of delay between when a proposal is made and when it can be executed */ - function changeVotingRules(uint minimumSharesToPassAVote, uint minutesForDebate) onlySelf public { + function changeVotingRules(uint minimumSharesToPassAVote, uint minutesForDebate) public onlySelf { if (minimumSharesToPassAVote == 0 ) { minimumSharesToPassAVote = 1; } @@ -148,17 +148,17 @@ contract DelegatedShareholderAssociation is TokenRecipient { /** * Add Proposal * - * Propose to send `weiAmount / 1e18` ether to `beneficiary` for `jobDescription`. `transactionBytecode ? Contains : Does not contain` code. + * Propose to send `weiAmount / 1e18` ether to `beneficiary` for `jobMetadataHash`. `transactionBytecode ? Contains : Does not contain` code. * * @param beneficiary who to send the ether to * @param weiAmount amount of ether to send, in wei - * @param jobDescription Description of job + * @param jobMetadataHash Hash of job metadata (IPFS) * @param transactionBytecode bytecode of transaction */ function newProposal( address beneficiary, uint weiAmount, - string jobDescription, + bytes jobMetadataHash, bytes transactionBytecode ) public @@ -169,13 +169,13 @@ contract DelegatedShareholderAssociation is TokenRecipient { Proposal storage p = proposals[proposalID]; p.recipient = beneficiary; p.amount = weiAmount; - p.description = jobDescription; + p.metadataHash = jobMetadataHash; p.proposalHash = keccak256(beneficiary, weiAmount, transactionBytecode); p.votingDeadline = now + debatingPeriodInMinutes * 1 minutes; p.executed = false; p.proposalPassed = false; p.numberOfVotes = 0; - ProposalAdded(proposalID, beneficiary, weiAmount, jobDescription); + ProposalAdded(proposalID, beneficiary, weiAmount, jobMetadataHash); numProposals = proposalID+1; return proposalID; @@ -184,25 +184,25 @@ contract DelegatedShareholderAssociation is TokenRecipient { /** * Add proposal in Ether * - * Propose to send `etherAmount` ether to `beneficiary` for `jobDescription`. `transactionBytecode ? Contains : Does not contain` code. + * Propose to send `etherAmount` ether to `beneficiary` for `jobMetadataHash`. `transactionBytecode ? Contains : Does not contain` code. * This is a convenience function to use if the amount to be given is in round number of ether units. * * @param beneficiary who to send the ether to * @param etherAmount amount of ether to send - * @param jobDescription Description of job + * @param jobMetadataHash Hash of job metadata (IPFS) * @param transactionBytecode bytecode of transaction */ function newProposalInEther( address beneficiary, uint etherAmount, - string jobDescription, + bytes jobMetadataHash, bytes transactionBytecode ) public onlyShareholders returns (uint proposalID) { - return newProposal(beneficiary, etherAmount * 1 ether, jobDescription, transactionBytecode); + return newProposal(beneficiary, etherAmount * 1 ether, jobMetadataHash, transactionBytecode); } /** diff --git a/migrations/2_deploy_test_token_and_dao.js b/migrations/2_deploy_test_token_and_dao.js new file mode 100644 index 0000000..d9529fa --- /dev/null +++ b/migrations/2_deploy_test_token_and_dao.js @@ -0,0 +1,13 @@ +/* global artifacts: false */ + +const TestToken = artifacts.require('./TestToken.sol') +const TestDAO = artifacts.require('./TestDAO.sol') + +module.exports = (deployer, network) => { + if (network === 'development') { + deployer.deploy(TestToken) + .then(() => { + return deployer.deploy(TestDAO, TestToken.address, Math.pow(10, 18) * 1000000, 60 * 24 * 7) + }) + } +} diff --git a/migrations/2_deploy_token_and_dao.js b/migrations/3_deploy_token_and_dao.js similarity index 100% rename from migrations/2_deploy_token_and_dao.js rename to migrations/3_deploy_token_and_dao.js diff --git a/test/test-dao.js b/test/test-dao.js new file mode 100644 index 0000000..9371851 --- /dev/null +++ b/test/test-dao.js @@ -0,0 +1,38 @@ +/* global artifacts:false, it:false, contract:false, assert:false */ + +const TestDAO = artifacts.require('TestDAO') +const TestToken = artifacts.require('TestToken') + +const BigNumber = require('bignumber.js') + +contract('TestDAO', (accounts) => { + it('should not allow delegation of more shares than owned', () => { + return TestDAO + .deployed() + .then(daoInstance => { + return daoInstance.setDelegateAndLockTokens.call((new BigNumber(Math.pow(10, 18 + 7)).mul(3)), accounts[1]) + }) + .then(ret => { + assert.equal(true, false, 'Delegation was allowed without shares') + }) + .catch(err => { + assert.equal(err.message, 'VM Exception while processing transaction: revert', 'Incorrect error') + }) + }) + + it('should allow share delegation after token allowance', () => { + const amount = new BigNumber(Math.pow(10, 18 + 7)) + return TestDAO + .deployed() + .then(daoInstance => { + return TestToken + .deployed() + .then(tokenInstance => { + return tokenInstance.approve.sendTransaction(daoInstance.address, amount) + }) + .then(() => { + return daoInstance.setDelegateAndLockTokens.call(amount, accounts[1]) + }) + }) + }) +}) diff --git a/test/test-token.js b/test/test-token.js new file mode 100644 index 0000000..6080918 --- /dev/null +++ b/test/test-token.js @@ -0,0 +1,18 @@ +/* global artifacts:false, it:false, contract:false, assert:false */ + +const TestToken = artifacts.require('TestToken') + +const BigNumber = require('bignumber.js') + +contract('TestToken', (accounts) => { + it('should set correct balance', () => { + return TestToken + .deployed() + .then(tokenInstance => { + return tokenInstance.balanceOf.call(accounts[0]) + }) + .then(amount => { + assert.equal(amount.equals(new BigNumber(2 * Math.pow(10, 18 + 7))), true, 'Incorrect amount') + }) + }) +}) diff --git a/test/dao.js b/test/wyvern-dao.js similarity index 63% rename from test/dao.js rename to test/wyvern-dao.js index 5f4821c..ddd0a3d 100644 --- a/test/dao.js +++ b/test/wyvern-dao.js @@ -23,16 +23,32 @@ contract('WyvernDAO', (accounts) => { }) }) + it('should have the right address', () => { + return WyvernDAO + .deployed() + .then(daoInstance => { + return WyvernToken + .deployed() + .then(tokenInstance => { + return daoInstance.sharesTokenAddress.call() + .then(address => { + assert.equal(address, tokenInstance.address, 'Incorrect token address') + }) + }) + }) + }) + it('should not allow release twice', () => { return WyvernToken .deployed() .then(tokenInstance => { - return tokenInstance.releaseTokens.call(tokenInstance.address) + return tokenInstance.releaseTokens.sendTransaction(tokenInstance.address) }) .then(() => { assert.equal(true, false, 'Tokens were released twice!') }) - .catch(() => { + .catch(err => { + assert.equal(err.message, 'VM Exception while processing transaction: revert', 'Incorrect error') }) }) }) diff --git a/test/token.js b/test/wyvern-token.js similarity index 86% rename from test/token.js rename to test/wyvern-token.js index d40f50f..49c9ff1 100644 --- a/test/token.js +++ b/test/wyvern-token.js @@ -117,7 +117,7 @@ contract('WyvernToken', (accounts) => { }) }) - it('should credit valid UTXO', () => { + it('should credit valid UTXO, with correct amount, only once', () => { const utxo = utxoSet[35997] const hash = hashUTXO(utxo) const proof = utxoMerkleTree.getHexProof(Buffer.from(hash.slice(2), 'hex')) @@ -133,35 +133,22 @@ contract('WyvernToken', (accounts) => { .deployed() .then(instance => { return instance.redeemUTXO.call('0x' + utxo.txid, utxo.outputIndex, utxo.satoshis, proof, pubKey, keyPair.compressed, v, r, s) - }) - .then(amount => { - amount = amount.toNumber() - assert.equal(amount, utxo.satoshis * Math.pow(10, 11), 'UTXO was not credited correctly!') - }) - }) - - it('should credit valid UTXO only once', () => { - const utxo = utxoSet[35997] - const hash = hashUTXO(utxo) - const proof = utxoMerkleTree.getHexProof(Buffer.from(hash.slice(2), 'hex')) - const keyPair = bitcoin.ECPair.fromWIF('WsUAyHvNaCyEcK8bFvzENF8wQe9zumSpJQbqMjmkwtDeYo4cqVsp', network) - const ethAddr = accounts[0].slice(2) - const hashBuf = bitcoin.crypto.sha256(Buffer.from(ethAddr, 'hex')) - var { r, s, v } = ecsign(hashBuf, keyPair.d.toBuffer()) - r = '0x' + r.toString('hex') - s = '0x' + s.toString('hex') - const pubKey = '0x' + keyPair.Q.affineX.toBuffer(32).toString('hex') + keyPair.Q.affineY.toBuffer(32).toString('hex') - - return WyvernToken - .deployed() - .then(instance => { - return instance.redeemUTXO.call('0x' + utxo.txid, utxo.outputIndex, utxo.satoshis, proof, pubKey, keyPair.compressed, v, r, s) - }) - .then(() => { - assert.equal(false, true, 'UTXO was credited twice!') - }) - .catch(() => { - assert.equal(true, true, 'Error not thrown') + .then(amount => { + amount = amount.toNumber() + assert.equal(amount, utxo.satoshis * Math.pow(10, 11), 'UTXO was not credited correctly!') + }) + .then(() => { + return instance.redeemUTXO.sendTransaction('0x' + utxo.txid, utxo.outputIndex, utxo.satoshis, proof, pubKey, keyPair.compressed, v, r, s) + }) + .then(() => { + return instance.redeemUTXO.call('0x' + utxo.txid, utxo.outputIndex, utxo.satoshis, proof, pubKey, keyPair.compressed, v, r, s) + .then(() => { + assert.equal(false, true, 'UTXO was credited twice!') + }) + .catch(err => { + assert.equal(err.message, 'VM Exception while processing transaction: revert', 'Incorrect error') + }) + }) }) }) @@ -183,8 +170,8 @@ contract('WyvernToken', (accounts) => { return instance.redeemUTXO.call('0x' + utxo.txid, utxo.outputIndex + 1, utxo.satoshis, proof, pubKey, keyPair.compressed, v, r, s) .then(amount => { assert.equal(false, true, 'UTXO was credited!') - }).catch(() => { - assert.equal(true, true, 'Error not thrown') + }).catch(err => { + assert.equal(err.message, 'VM Exception while processing transaction: revert', 'Incorrect error') }) }) })