diff --git a/contracts/Contribution.sol b/contracts/Contribution.sol new file mode 100644 index 0000000..919b37e --- /dev/null +++ b/contracts/Contribution.sol @@ -0,0 +1,126 @@ +pragma solidity ^0.4.19; + +import "zeppelin-solidity/contracts/token/ERC721/ERC721Token.sol"; +import './upgradeable/Upgradeable.sol'; + +// ToDo: only load interfaces +import './Token.sol'; +import './Contributors.sol'; + +contract Contribution is Upgradeable, ERC721Token { + + struct ContributionData { + address contributor; + uint amount; + bool claimed; + bytes32 hashDigest; + uint8 hashFunction; + uint8 hashSize; + string tokenMetadataURL; + uint claimAfterBlock; + bool exists; + } + string internal name_; + string internal symbol_; + + mapping(uint256 => address) contributionOwner; + mapping(address => uint256[]) ownedContributions; + + mapping(uint256 => ContributionData) public contributions; + uint256 public contributionsCount; + + event ContributionAdded(uint256 id, address indexed contributor, uint256 amount); + event ContributionClaimed(uint256 id, address indexed contributor, uint256 amount); + + modifier coreOnly() { + require(contributorsContract().addressIsCore(msg.sender)); + _; + } + modifier contributorOnly() { + require(contributorsContract().addressExists(msg.sender)); + _; + } + + function contributorsContract() view public returns (Contributors) { + return Contributors(registry.getProxyFor('Contributors')); + } + + function tokenContract() view public returns (Token) { + return Token(registry.getProxyFor('Token')); + } + + function name() external view returns (string) { + return name_; + } + + function symbol() external view returns (string) { + return symbol_; + } + + function ownerOf(uint256 contributionId) public view returns (address) { + require(exists(contributionId)); + return contributions[contributionId].contributor; + } + + function balanceOf(address contributor) public view returns (uint) { + return ownedContributions[contributor].length; + } + + function tokenOfOwnerByIndex(address contributor, uint index) public view returns (uint) { + return ownedContributions[contributor][index]; + } + + function tokenMetadata(uint contributionId) public view returns (string) { + return contributions[contributionId].tokenMetadataURL; + } + + function getContribution(uint contributionId) public view returns (uint256 id, address contributor, uint256 amount, bool claimed, bytes32 hashDigest, uint8 hashFunction, uint8 hashSize, uint claimAfterBlock, bool exists) { + id = contributionId; + ContributionData storage c = contributions[id]; + return ( + id, + c.contributor, + c.amount, + c.claimed, + c.hashDigest, + c.hashFunction, + c.hashSize, + c.claimAfterBlock, + c.exists + ); + } + + function add(uint256 amount, address contributor, uint256 blocksToWait) public coreOnly { + uint contributionId = contributionsCount + 1; + ContributionData storage c = contributions[contributionId]; + c.exists = true; + c.amount = amount; + c.claimed = false; + c.contributor = contributor; + c.claimAfterBlock = block.number + blocksToWait; + + contributionsCount++; + + contributionOwner[contributionId] = contributor; + ownedContributions[contributor].push(contributionId); + + ContributionAdded(contributionId, contributor, amount); + } + + function claim(uint256 contributionId) public { + ContributionData storage c = contributions[contributionId]; + require(c.exists); + require(!c.claimed); + require(block.number > c.claimAfterBlock); + c.claimed = true; + tokenContract().mintFor(c.contributor, c.amount, contributionId); + + ContributionClaimed(contributionId, c.contributor, c.amount); + } + + function exists(uint256 contributionId) view public returns (bool) { + return contributions[contributionId].exists; + } + + +} diff --git a/contracts/Operator.sol b/contracts/Operator.sol index 390f7af..d4b1542 100644 --- a/contracts/Operator.sol +++ b/contracts/Operator.sol @@ -3,6 +3,7 @@ pragma solidity ^0.4.18; // ToDo: only load interfaces import './Token.sol'; import './Contributors.sol'; +import './Contribution.sol'; contract Operator is Upgradeable { @@ -47,6 +48,9 @@ contract Operator is Upgradeable { function tokenContract() view public returns (Token) { return Token(registry.getProxyFor('Token')); } + function contributionContract() view public returns (Contribution) { + return Contribution(registry.getProxyFor('Contribution')); + } function contributorsCount() view public returns (uint) { return contributorsContract().contributorsCount(); @@ -117,12 +121,11 @@ contract Operator is Upgradeable { } function executeProposal(uint proposalId) private { - var p = proposals[proposalId]; require(!p.executed); require(p.votesCount >= p.votesNeeded); address recipientAddress = contributorsContract().getContributorAddressById(p.contributorId); - tokenContract().mintFor(recipientAddress, p.amount, proposalId); + contributionContract().add(p.amount, recipientAddress, 0); p.executed = true; ProposalExecuted(proposalId, p.contributorId, p.amount); } diff --git a/contracts/Token.sol b/contracts/Token.sol index 9d81a9f..61ca124 100644 --- a/contracts/Token.sol +++ b/contracts/Token.sol @@ -17,7 +17,7 @@ contract Token is Upgradeable, BasicToken { decimals = 18; } - function mintFor(address contributorAccount, uint256 amount, uint proposalId) onlyRegistryContractFor('Operator') public { + function mintFor(address contributorAccount, uint256 amount, uint proposalId) onlyRegistryContractFor('Contribution') public { totalSupply_ = totalSupply_.add(amount); balances[contributorAccount] = balances[contributorAccount].add(amount); diff --git a/lib/abis/Contribution.json b/lib/abis/Contribution.json new file mode 100644 index 0000000..64b488c --- /dev/null +++ b/lib/abis/Contribution.json @@ -0,0 +1 @@ +[{"constant":false,"inputs":[{"name":"_to","type":"address"},{"name":"_tokenId","type":"uint256"}],"name":"approve","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"totalSupply","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"name":"","type":"uint256"}],"name":"contributions","outputs":[{"name":"contributor","type":"address"},{"name":"amount","type":"uint256"},{"name":"claimed","type":"bool"},{"name":"hashDigest","type":"bytes32"},{"name":"hashFunction","type":"uint8"},{"name":"hashSize","type":"uint8"},{"name":"tokenMetadataURL","type":"string"},{"name":"claimAfterBlock","type":"uint256"},{"name":"exists","type":"bool"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"name":"_tokenId","type":"uint256"}],"name":"approvedFor","outputs":[{"name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"contributionsCount","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"name":"_owner","type":"address"}],"name":"tokensOf","outputs":[{"name":"","type":"uint256[]"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"implementation","outputs":[{"name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"_proxiedContractName","outputs":[{"name":"","type":"string"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"_to","type":"address"},{"name":"_tokenId","type":"uint256"}],"name":"transfer","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"name":"_tokenId","type":"uint256"}],"name":"takeOwnership","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"name":"sender","type":"address"}],"name":"initialize","outputs":[],"payable":true,"stateMutability":"payable","type":"function"},{"anonymous":false,"inputs":[{"indexed":false,"name":"id","type":"uint256"},{"indexed":true,"name":"contributor","type":"address"},{"indexed":false,"name":"amount","type":"uint256"}],"name":"ContributionAdded","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"name":"id","type":"uint256"},{"indexed":true,"name":"contributor","type":"address"},{"indexed":false,"name":"amount","type":"uint256"}],"name":"ContributionClaimed","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"_from","type":"address"},{"indexed":true,"name":"_to","type":"address"},{"indexed":false,"name":"_tokenId","type":"uint256"}],"name":"Transfer","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"_owner","type":"address"},{"indexed":true,"name":"_approved","type":"address"},{"indexed":false,"name":"_tokenId","type":"uint256"}],"name":"Approval","type":"event"},{"constant":true,"inputs":[],"name":"contributorsContract","outputs":[{"name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"tokenContract","outputs":[{"name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"name","outputs":[{"name":"","type":"string"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"symbol","outputs":[{"name":"","type":"string"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"name":"contributionId","type":"uint256"}],"name":"ownerOf","outputs":[{"name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"name":"contributor","type":"address"}],"name":"balanceOf","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"name":"contributor","type":"address"},{"name":"index","type":"uint256"}],"name":"tokenOfOwnerByIndex","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"name":"contributionId","type":"uint256"}],"name":"tokenMetadata","outputs":[{"name":"","type":"string"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"name":"contributionId","type":"uint256"}],"name":"getContribution","outputs":[{"name":"id","type":"uint256"},{"name":"contributor","type":"address"},{"name":"amount","type":"uint256"},{"name":"claimed","type":"bool"},{"name":"hashDigest","type":"bytes32"},{"name":"hashFunction","type":"uint8"},{"name":"hashSize","type":"uint8"},{"name":"claimAfterBlock","type":"uint256"},{"name":"exists","type":"bool"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"amount","type":"uint256"},{"name":"contributor","type":"address"},{"name":"blocksToWait","type":"uint256"}],"name":"add","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"name":"contributionId","type":"uint256"}],"name":"claim","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[{"name":"contributionId","type":"uint256"}],"name":"exists","outputs":[{"name":"","type":"bool"}],"payable":false,"stateMutability":"view","type":"function"}] \ No newline at end of file diff --git a/lib/abis/Operator.json b/lib/abis/Operator.json index f099710..fe21bd3 100644 --- a/lib/abis/Operator.json +++ b/lib/abis/Operator.json @@ -1 +1 @@ -[{"constant":true,"inputs":[{"name":"","type":"uint256"}],"name":"proposals","outputs":[{"name":"creatorAccount","type":"address"},{"name":"contributorId","type":"uint256"},{"name":"votesCount","type":"uint256"},{"name":"votesNeeded","type":"uint256"},{"name":"amount","type":"uint256"},{"name":"executed","type":"bool"},{"name":"ipfsHash","type":"bytes32"},{"name":"hashFunction","type":"uint8"},{"name":"hashSize","type":"uint8"},{"name":"exists","type":"bool"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"proposalsCount","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"implementation","outputs":[{"name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"_proxiedContractName","outputs":[{"name":"","type":"string"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"sender","type":"address"}],"name":"initialize","outputs":[],"payable":true,"stateMutability":"payable","type":"function"},{"anonymous":false,"inputs":[{"indexed":false,"name":"id","type":"uint256"},{"indexed":false,"name":"creatorAccount","type":"address"},{"indexed":false,"name":"contributorId","type":"uint256"},{"indexed":false,"name":"amount","type":"uint256"}],"name":"ProposalCreated","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"name":"id","type":"uint256"},{"indexed":false,"name":"voterId","type":"uint256"},{"indexed":false,"name":"totalVotes","type":"uint256"}],"name":"ProposalVoted","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"name":"id","type":"uint256"},{"indexed":false,"name":"contributorId","type":"uint256"},{"indexed":false,"name":"amount","type":"uint256"}],"name":"ProposalExecuted","type":"event"},{"constant":true,"inputs":[],"name":"contributorsContract","outputs":[{"name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"tokenContract","outputs":[{"name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"contributorsCount","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"coreContributorsCount","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"contributorId","type":"uint256"},{"name":"amount","type":"uint256"},{"name":"ipfsHash","type":"bytes32"},{"name":"hashFunction","type":"uint8"},{"name":"hashSize","type":"uint8"}],"name":"addProposal","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[{"name":"proposalId","type":"uint256"}],"name":"getProposal","outputs":[{"name":"id","type":"uint256"},{"name":"creatorAccount","type":"address"},{"name":"contributorId","type":"uint256"},{"name":"votesCount","type":"uint256"},{"name":"votesNeeded","type":"uint256"},{"name":"amount","type":"uint256"},{"name":"executed","type":"bool"},{"name":"ipfsHash","type":"bytes32"},{"name":"hashFunction","type":"uint8"},{"name":"hashSize","type":"uint8"},{"name":"voterIds","type":"uint256[]"},{"name":"exists","type":"bool"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"proposalId","type":"uint256"}],"name":"vote","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"name":"_proposalIds","type":"uint256[]"}],"name":"batchVote","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"}] \ No newline at end of file +[{"constant":true,"inputs":[{"name":"","type":"uint256"}],"name":"proposals","outputs":[{"name":"creatorAccount","type":"address"},{"name":"contributorId","type":"uint256"},{"name":"votesCount","type":"uint256"},{"name":"votesNeeded","type":"uint256"},{"name":"amount","type":"uint256"},{"name":"executed","type":"bool"},{"name":"ipfsHash","type":"bytes32"},{"name":"hashFunction","type":"uint8"},{"name":"hashSize","type":"uint8"},{"name":"exists","type":"bool"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"proposalsCount","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"implementation","outputs":[{"name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"_proxiedContractName","outputs":[{"name":"","type":"string"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"sender","type":"address"}],"name":"initialize","outputs":[],"payable":true,"stateMutability":"payable","type":"function"},{"anonymous":false,"inputs":[{"indexed":false,"name":"id","type":"uint256"},{"indexed":false,"name":"creatorAccount","type":"address"},{"indexed":false,"name":"contributorId","type":"uint256"},{"indexed":false,"name":"amount","type":"uint256"}],"name":"ProposalCreated","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"name":"id","type":"uint256"},{"indexed":false,"name":"voterId","type":"uint256"},{"indexed":false,"name":"totalVotes","type":"uint256"}],"name":"ProposalVoted","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"name":"id","type":"uint256"},{"indexed":false,"name":"contributorId","type":"uint256"},{"indexed":false,"name":"amount","type":"uint256"}],"name":"ProposalExecuted","type":"event"},{"constant":true,"inputs":[],"name":"contributorsContract","outputs":[{"name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"tokenContract","outputs":[{"name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"contributionContract","outputs":[{"name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"contributorsCount","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"coreContributorsCount","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"contributorId","type":"uint256"},{"name":"amount","type":"uint256"},{"name":"ipfsHash","type":"bytes32"},{"name":"hashFunction","type":"uint8"},{"name":"hashSize","type":"uint8"}],"name":"addProposal","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[{"name":"proposalId","type":"uint256"}],"name":"getProposal","outputs":[{"name":"id","type":"uint256"},{"name":"creatorAccount","type":"address"},{"name":"contributorId","type":"uint256"},{"name":"votesCount","type":"uint256"},{"name":"votesNeeded","type":"uint256"},{"name":"amount","type":"uint256"},{"name":"executed","type":"bool"},{"name":"ipfsHash","type":"bytes32"},{"name":"hashFunction","type":"uint8"},{"name":"hashSize","type":"uint8"},{"name":"voterIds","type":"uint256[]"},{"name":"exists","type":"bool"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"proposalId","type":"uint256"}],"name":"vote","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"name":"_proposalIds","type":"uint256[]"}],"name":"batchVote","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"}] \ No newline at end of file diff --git a/lib/contracts/contribution.js b/lib/contracts/contribution.js new file mode 100644 index 0000000..c7f933d --- /dev/null +++ b/lib/contracts/contribution.js @@ -0,0 +1,51 @@ +const ethers = require('ethers'); +const RSVP = require('rsvp'); + +const ContributionSerializer = require('../serializers/contribution'); +const Base = require('./base'); + +class Contribution extends Base { + all() { + return this.functions.contributionsCount() + .then((count) => { + count = count.toNumber(); + let contributions = []; + + for (let id = 1; id <= count; id++) { + contributions.push(this.getById(id)); + } + + return RSVP.all(contributions); + }); + } + + getById(id) { + id = ethers.utils.bigNumberify(id); + + return this.functions.getContribution(id) + .then((data) => { + return this.ipfs.catAndMerge(data, ContributionSerializer.deserialize); + }); + + } + + getByContributor(contributor) { + return this.functions.balanceOf(contributor) + then((balance) => { + count = balance.toNumber(); + + let contributions = []; + + for (let index = 0; index <= count; index++) { + this.functions.tokenOfOwnerByIndex(contributor, index) + .then((id) => { + contributions.push(this.getById(id)); + }); + } + + return RSVP.all(contributions); + }); + } +} + +module.exports = Contribution; diff --git a/lib/contracts/index.js b/lib/contracts/index.js index a4a1792..ff3dc15 100644 --- a/lib/contracts/index.js +++ b/lib/contracts/index.js @@ -1,5 +1,6 @@ module.exports = { Contributors: require('./contributor'), + Contribution: require('./contribution'), Operator: require('./operator'), Token: require('./token'), Registry: require('./registry') diff --git a/lib/kredits.js b/lib/kredits.js index 9300179..6d17aca 100644 --- a/lib/kredits.js +++ b/lib/kredits.js @@ -5,6 +5,7 @@ const Preflight = require('./utils/preflight'); const ABIS = { Contributors: require('./abis/Contributors.json'), + Contribution: require('./abis/Contribution.json'), Operator: require('./abis/Operator.json'), Registry: require('./abis/Registry.json'), Token: require('./abis/Token.json') @@ -76,6 +77,10 @@ class Kredits { return this.contractFor('token'); } + get Contribution() { + return this.contractFor('contribution'); + } + // Should be private contractFor(name) { if (this.contracts[name]) { diff --git a/migrations/1528988660_contribution.js b/migrations/1528988660_contribution.js new file mode 100644 index 0000000..f6cb6a9 --- /dev/null +++ b/migrations/1528988660_contribution.js @@ -0,0 +1,13 @@ +var Registry = artifacts.require('./Registry.sol'); +var Contribution = artifacts.require('./Contribution.sol'); + +module.exports = function(deployer) { + deployer.deploy(Contribution).then(function(contribution) { + console.log('Registry address: ', Registry.address); + console.log('Contribution address: ', Contribution.address); + Registry.deployed().then(function(registry) { + registry.addVersion('Contribution', Contribution.address); + registry.createProxy('Contribution', 1); + }); + }); +}; diff --git a/scripts/build-json.js b/scripts/build-json.js index db00827..0fb057b 100644 --- a/scripts/build-json.js +++ b/scripts/build-json.js @@ -8,6 +8,7 @@ const addressesPath = path.join(libPath, 'addresses'); const files = [ 'Contributors', + 'Contribution', 'Operator', 'Registry', 'Token'