From 74ce08617b26a2dd3ff7b7e797b5f092aba6ee91 Mon Sep 17 00:00:00 2001 From: Kevin Siegler Date: Wed, 22 Jan 2020 17:21:32 -0600 Subject: [PATCH 1/2] feat(projects): add decoupled project functionality - add decentralized project entries - add and update issues directly without bounties - enable multi-fulfillment bounties BREAKING CHANGE: Projects contract updates --- apps/projects/arapp.json | 5 + apps/projects/contracts/Projects.sol | 300 +++++++++-------- .../contracts/test/BountiesEvents.sol | 58 ++-- apps/projects/test/Projects.test.js | 314 ++++++++++++------ 4 files changed, 405 insertions(+), 272 deletions(-) diff --git a/apps/projects/arapp.json b/apps/projects/arapp.json index 50835cc45..567742051 100644 --- a/apps/projects/arapp.json +++ b/apps/projects/arapp.json @@ -30,6 +30,11 @@ "id": "CURATE_ISSUES_ROLE", "params": [] }, + { + "name": "Create and update project issues", + "id": "CREATE_ISSUES_ROLE", + "params": [] + }, { "name": "Remove projects", "id": "REMOVE_REPO_ROLE", diff --git a/apps/projects/contracts/Projects.sol b/apps/projects/contracts/Projects.sol index 2ecdf8d02..64f130ef4 100644 --- a/apps/projects/contracts/Projects.sol +++ b/apps/projects/contracts/Projects.sol @@ -122,6 +122,7 @@ contract Projects is AragonApp, DepositableStorage { BountySettings public settings; Vault public vault; // Auth roles + bytes32 public constant CREATE_ISSUES_ROLE = keccak256("CREATE_ISSUES_ROLE"); bytes32 public constant FUND_ISSUES_ROLE = keccak256("FUND_ISSUES_ROLE"); bytes32 public constant REMOVE_ISSUES_ROLE = keccak256("REMOVE_ISSUES_ROLE"); bytes32 public constant ADD_REPO_ROLE = keccak256("ADD_REPO_ROLE"); @@ -145,7 +146,6 @@ contract Projects is AragonApp, DepositableStorage { string private constant ERROR_INVALID_AMOUNT = "INVALID_TOKEN_AMOUNT"; string private constant ERROR_ETH_CONTRACT = "WRONG_ETH_TOKEN"; string private constant ERROR_REPO_MISSING = "REPO_NOT_ADDED"; - string private constant ERROR_REPO_EXISTS = "REPO_ALREADY_ADDED"; string private constant ERROR_USER_APPLIED = "USER_ALREADY_APPLIED"; string private constant ERROR_NO_APPLICATION = "USER_APPLICATION_MISSING"; string private constant ERROR_NO_ERC721 = "ERC_721_FORBIDDEN"; @@ -175,8 +175,10 @@ contract Projects is AragonApp, DepositableStorage { } struct Repo { - mapping(uint256 => Issue) issues; uint index; + mapping(uint256 => Issue) issues; + bool decoupled; + string repoData; } struct AssignmentRequest { @@ -188,28 +190,25 @@ contract Projects is AragonApp, DepositableStorage { struct Issue { bytes32 repo; // This is the internal repo identifier uint256 number; // May be redundant tracking this - bool hasBounty; - bool fulfilled; address tokenContract; uint256 bountySize; + uint256 fulfilledAmount; uint256 priority; - address bountyWallet; // Not sure if we'll have a way to "retrieve" this value from status open bounties uint standardBountyId; address assignee; + string issueData; address[] applicants; //uint256 submissionQty; - uint256[] submissionIndices; mapping(address => AssignmentRequest) assignmentRequests; } // Fired when a repository is added to the registry. - event RepoAdded(bytes32 indexed repoId, uint index); + event RepoAdded(bytes32 indexed repoId, uint index, bool decoupled); + event RepoUpdated(bytes32 indexed repoId, uint index, string repoData); // Fired when a repository is removed from the registry. event RepoRemoved(bytes32 indexed repoId, uint index); - // Fired when a repo is updated in the registry - event RepoUpdated(bytes32 indexed repoId, uint newIndex); // Fired when a bounty is added to a repo - event BountyAdded(bytes32 repoId, uint256 issueNumber, uint256 bountySize, uint256 registryId, string ipfsHash); + event IssueUpdated(bytes32 repoId, uint256 issueNumber, uint256 bountySize, uint256 registryId, string ipfsHash); // Fired when a bounty is removed event BountyRemoved(bytes32 repoId, uint256 issueNumber, uint256 oldBountySize); // Fired when an issue is curated @@ -281,7 +280,7 @@ contract Projects is AragonApp, DepositableStorage { ) external auth(CHANGE_SETTINGS_ROLE) { require(_expMultipliers.length == _expLevels.length, ERROR_LENGTH_MISMATCH); - require(_isBountiesContractValid(_bountyAllocator), ERROR_STANDARD_BOUNTIES_NOT_CONTRACT); + require(isContract(_bountyAllocator), ERROR_STANDARD_BOUNTIES_NOT_CONTRACT); settings.expLevels.length = 0; settings.expMultipliers.length = 0; for (uint i = 0; i < _expLevels.length; i++) { @@ -299,14 +298,14 @@ contract Projects is AragonApp, DepositableStorage { * @param _repoId The id of the repo in the projects registry */ function getIssue(bytes32 _repoId, uint256 _issueNumber) external view isInitialized - returns(bool hasBounty, uint standardBountyId, bool fulfilled, uint balance, address assignee) + returns(uint standardBountyId, uint256 fulfilled, uint balance, address assignee, string data) { Issue storage issue = repos[_repoId].issues[_issueNumber]; - hasBounty = issue.hasBounty; - fulfilled = issue.fulfilled; + fulfilled = issue.fulfilledAmount; standardBountyId = issue.standardBountyId; balance = issue.bountySize; assignee = issue.assignee; + data = issue.issueData; } /** @@ -319,12 +318,16 @@ contract Projects is AragonApp, DepositableStorage { /** * @notice Get an entry from the registry. * @param _repoId The id of the repo in the projects registry - * @return index the repo registry index + * @return index the repo registry index, number of open bounties, decoupled bool, repo metadata */ - function getRepo(bytes32 _repoId) external view isInitialized returns (uint256 index, uint256 openIssueCount) { + function getRepo(bytes32 _repoId) external view isInitialized + returns (uint256 index, uint256 openIssueCount, bool decoupled, string repoData) + { require(isRepoAdded(_repoId), ERROR_REPO_MISSING); index = repos[_repoId].index; openIssueCount = openBounties[_repoId]; + decoupled = repos[_repoId].decoupled; + repoData = repos[_repoId].repoData; } /** @@ -357,20 +360,25 @@ contract Projects is AragonApp, DepositableStorage { // Repository functions /////////////////////// /** - * @notice Add repository + * @notice Add new Project: `_projectData`. * @param _repoId The id of the repo in the projects registry - * @return index for the added repo at the registry */ - function addRepo( - bytes32 _repoId - ) external auth(ADD_REPO_ROLE) returns (uint index) + function setRepo( + bytes32 _repoId, + bool _decoupled, + string _projectData + ) external auth(ADD_REPO_ROLE) + { + _setRepo(_repoId, _decoupled, _projectData); + } + + function setIssue( + bytes32 _repoId, + uint256 _issueNumber, + string _issueData + ) external auth(CREATE_ISSUES_ROLE) { - require(!isRepoAdded(_repoId), ERROR_REPO_EXISTS); - repoIndex[repoIndexLength] = _repoId; - repos[_repoId].index = repoIndexLength++; - //repos[_repoId].index = repoIndex.push(_repoId) - 1; - emit RepoAdded(_repoId, repos[_repoId].index); - return repoIndexLength - 1; + _setIssue(_repoId, _issueNumber, 0, address(0), 0, _issueData); } /** @@ -379,7 +387,7 @@ contract Projects is AragonApp, DepositableStorage { */ function removeRepo( bytes32 _repoId - ) external auth(REMOVE_REPO_ROLE) returns (bool success) + ) external auth(REMOVE_REPO_ROLE) { require(isRepoAdded(_repoId), ERROR_REPO_MISSING); require(openBounties[_repoId] == 0, ERROR_PENDING_BOUNTIES); @@ -393,7 +401,6 @@ contract Projects is AragonApp, DepositableStorage { repoIndexLength--; emit RepoRemoved(_repoId, rowToDelete); - return true; } /////////////////// @@ -414,8 +421,7 @@ contract Projects is AragonApp, DepositableStorage { { Issue storage issue = repos[_repoId].issues[_issueNumber]; - require(!issue.fulfilled,ERROR_BOUNTY_FULFILLED); - require(issue.hasBounty, ERROR_ISSUE_INACTIVE); + require(issue.bountySize > 0, ERROR_ISSUE_INACTIVE); require(issue.assignee != address(-1), ERROR_OPEN_BOUNTY); require(issue.assignmentRequests[msg.sender].exists == false, ERROR_USER_APPLIED); @@ -490,17 +496,22 @@ contract Projects is AragonApp, DepositableStorage { { Issue storage issue = repos[_repoId].issues[_issueNumber]; - require(!issue.fulfilled,ERROR_BOUNTY_FULFILLED); require(issue.assignee != address(0), ERROR_ISSUE_INACTIVE); if (_approved) { - uint256 tokenTotal; for (uint256 i = 0; i < _tokenAmounts.length; i++) { - tokenTotal = tokenTotal.add(_tokenAmounts[i]); + issue.fulfilledAmount = issue.fulfilledAmount.add(_tokenAmounts[i]); + if (_tokenAmounts[i] > issue.bountySize) { + // This condition exists to allow for the bounty total beyond the initial size + // to be awarded, so the "full" bounty can be awarded when 3rd parties have contributed. + // The standard bounties contract will revert if the amount exceeds the total bounty + issue.bountySize = 0; + } else { + // don't need safemath because of the condition check + issue.bountySize -= _tokenAmounts[i]; + } } - require(tokenTotal >= issue.bountySize, ERROR_INVALID_AMOUNT); - issue.fulfilled = true; bountiesRegistry.acceptFulfillment( address(this), issue.standardBountyId, @@ -540,9 +551,8 @@ contract Projects is AragonApp, DepositableStorage { { Issue storage issue = repos[_repoId].issues[_issueNumber]; - require(!issue.fulfilled,ERROR_BOUNTY_FULFILLED); - require(issue.hasBounty, ERROR_ISSUE_INACTIVE); - + require(issue.bountySize > 0, ERROR_ISSUE_INACTIVE); + issue.issueData = _data; bountiesRegistry.changeData( address(this), issue.standardBountyId, @@ -641,34 +651,17 @@ contract Projects is AragonApp, DepositableStorage { string _description ) public payable auth(FUND_ISSUES_ROLE) { - // ensure the transvalue passed equals transaction value - //checkTransValueEqualsMessageValue(msg.value, _bountySizes,_tokenBounties); - string memory ipfsHash; - uint standardBountyId; - require(bytes(_ipfsAddresses).length == (CID_LENGTH * _bountySizes.length), ERROR_CID_LENGTH); - - for (uint i = 0; i < _bountySizes.length; i++) { - ipfsHash = getHash(_ipfsAddresses, i); - - // submit the bounty to the StandardBounties contract - standardBountyId = _issueBounty( - ipfsHash, - _deadlines[i], - _tokenContracts[i], - _tokenTypes[i], - _bountySizes[i] - ); - - //Add bounty to local registry - _addBounty( - _repoIds[i], - _issueNumbers[i], - standardBountyId, - _tokenContracts[i], - _bountySizes[i], - ipfsHash - ); - } + _addBounties( + _repoIds, + _issueNumbers, + _bountySizes, + _deadlines, + _tokenTypes, + _tokenContracts, + _ipfsAddresses, + _description, + false + ); } /** @@ -693,35 +686,17 @@ contract Projects is AragonApp, DepositableStorage { string _description ) public payable auth(FUND_OPEN_ISSUES_ROLE) { - string memory ipfsHash; - uint standardBountyId; - - for (uint i = 0; i < _bountySizes.length; i++) { - ipfsHash = getHash(_ipfsAddresses, i); - - // submit the bounty to the StandardBounties contract - standardBountyId = _issueBounty( - ipfsHash, - _deadlines[i], - _tokenContracts[i], - _tokenTypes[i], - _bountySizes[i] - ); - - //Add bounty to local registry - _addBounty( - _repoIds[i], - _issueNumbers[i], - standardBountyId, - _tokenContracts[i], - _bountySizes[i], - ipfsHash - ); - - repos[_repoIds[i]].issues[_issueNumbers[i]].assignee = address(-1); - emit AwaitingSubmissions(_repoIds[i], _issueNumbers[i]); - } - + _addBounties( + _repoIds, + _issueNumbers, + _bountySizes, + _deadlines, + _tokenTypes, + _tokenContracts, + _ipfsAddresses, + _description, + true + ); } /** @@ -781,40 +756,68 @@ contract Projects is AragonApp, DepositableStorage { // Internal functions /////////////////////// - /** - * @dev checks the hashed contract code to ensure it matches the provided hash - */ - function _isBountiesContractValid(address _bountyRegistry) internal view returns(bool) { - if (_bountyRegistry == address(0)) { - return false; - } - if (_bountyRegistry == address(bountiesRegistry)) { - return true; - } - uint256 size; - // solium-disable-next-line security/no-inline-assembly - assembly { size := extcodesize(_bountyRegistry) } - if (size != 23406) { - return false; + function _setRepo( + bytes32 _repoId, + bool _decoupled, + string _repoData + ) internal + { + repos[_repoId].repoData = _repoData; + if (!isRepoAdded(_repoId)) { + repoIndex[repoIndexLength] = _repoId; + repos[_repoId].index = repoIndexLength++; + repos[_repoId].decoupled = _decoupled; + emit RepoAdded(_repoId, repos[_repoId].index, _decoupled); + } else { + emit RepoUpdated(_repoId, repos[_repoId].index, _repoData); } - uint256 segments = 4; - uint256 segmentLength = size / segments; - bytes memory registryCode = new bytes(segmentLength); - bytes32[4] memory validRegistryHashes = [ - bytes32(0x9904de0ff2a8144b30f80f0de9184731b7c39116b1f021bad12dcbb740f8371d), - bytes32(0xd2319fa5b8b5614a3634c84ff340d27fa6e5921162e44bc2256f379ad86608f3), - bytes32(0x0fd4c8d32b2c21b41989666a6d19f7a5f4987ae6d915dd96698de62db8a79643), - bytes32(0x6af9efdc22f9352086c68a7b5c270db4f0fdc2b5ab18984a2d17b92ae327e144) - ]; - for (uint256 i = 0; i < segments; i++) { - // solium-disable-next-line security/no-inline-assembly - assembly{ extcodecopy(_bountyRegistry,add(0x20,registryCode),div(mul(i,segmentLength),segments),segmentLength) } - if (validRegistryHashes[i] != keccak256(registryCode)) { - return false; + } + + function _addBounties( + bytes32[] _repoIds, + uint256[] _issueNumbers, + uint256[] _bountySizes, + uint256[] _deadlines, + uint256[] _tokenTypes, + address[] _tokenContracts, + string _ipfsAddresses, + string _description, + bool _open + ) internal + { + // ensure the transvalue passed equals transaction value + //checkTransValueEqualsMessageValue(msg.value, _bountySizes,_tokenBounties); + string memory ipfsHash; + uint standardBountyId; + require(bytes(_ipfsAddresses).length == (CID_LENGTH * _bountySizes.length), ERROR_CID_LENGTH); + + for (uint i = 0; i < _bountySizes.length; i++) { + ipfsHash = getHash(_ipfsAddresses, i); + + // submit the bounty to the StandardBounties contract + standardBountyId = _issueBounty( + ipfsHash, + _deadlines[i], + _tokenContracts[i], + _tokenTypes[i], + _bountySizes[i] + ); + + //Add bounty to local registry + _addBounty( + _repoIds[i], + _issueNumbers[i], + standardBountyId, + _tokenContracts[i], + _bountySizes[i], + ipfsHash + ); + + if (_open) { + repos[_repoIds[i]].issues[_issueNumbers[i]].assignee = address(-1); + emit AwaitingSubmissions(_repoIds[i], _issueNumbers[i]); } } - - return true; } /** @@ -926,31 +929,48 @@ contract Projects is AragonApp, DepositableStorage { uint256 _bountySize, string _ipfsHash ) internal + { + //Issue storage issue = repos[_repoId].issues[_issueNumber]; + //require(isRepoAdded(_repoId), ERROR_REPO_MISSING); + openBounties[_repoId] = openBounties[_repoId].add(1); + + _setIssue( + _repoId, + _issueNumber, + _standardBountyId, + _tokenContract, + _bountySize, + _ipfsHash + ); + } + + function _setIssue( + bytes32 _repoId, + uint256 _issueNumber, + uint _standardBountyId, + address _tokenContract, + uint256 _bountySize, + string _ipfsHash + ) internal { address[] memory emptyAddressArray = new address[](0); - uint256[] memory emptySubmissionIndexArray = new uint256[](0); //Issue storage issue = repos[_repoId].issues[_issueNumber]; require(isRepoAdded(_repoId), ERROR_REPO_MISSING); - require(!repos[_repoId].issues[_issueNumber].hasBounty, ERROR_ISSUE_ACTIVE); + require(repos[_repoId].issues[_issueNumber].bountySize == 0, ERROR_ISSUE_ACTIVE); repos[_repoId].issues[_issueNumber] = Issue( _repoId, _issueNumber, - true, - false, _tokenContract, _bountySize, + 0, 999, - ETH, _standardBountyId, ETH, - emptyAddressArray, - //address(0), - //0, - emptySubmissionIndexArray + _ipfsHash, + emptyAddressArray ); - openBounties[_repoId] = openBounties[_repoId].add(1); - emit BountyAdded( + emit IssueUpdated( _repoId, _issueNumber, _bountySize, @@ -974,9 +994,7 @@ contract Projects is AragonApp, DepositableStorage { ) internal { Issue storage issue = repos[_repoId].issues[_issueNumber]; - require(issue.hasBounty, ERROR_BOUNTY_REMOVED); - require(!issue.fulfilled, ERROR_BOUNTY_FULFILLED); - issue.hasBounty = false; + require(issue.bountySize > 0, ERROR_BOUNTY_REMOVED); uint256[] memory originalAmount = new uint256[](1); originalAmount[0] = issue.bountySize; bountiesRegistry.drainBounty( diff --git a/apps/projects/contracts/test/BountiesEvents.sol b/apps/projects/contracts/test/BountiesEvents.sol index b30f844aa..aaba3b561 100644 --- a/apps/projects/contracts/test/BountiesEvents.sol +++ b/apps/projects/contracts/test/BountiesEvents.sol @@ -3,32 +3,38 @@ pragma solidity ^0.4.24; interface BountiesEvents { - /* - * Functions - */ + /* + * Functions + */ - function fulfillBounty( - address _sender, - uint _bountyId, - address[] _fulfillers, - string _data - ) external; - /* - * Events - */ + function fulfillBounty( + address _sender, + uint _bountyId, + address[] _fulfillers, + string _data + ) external; - event BountyIssued(uint _bountyId, address _creator, address [] _issuers, address [] _approvers, string _data, uint _deadline, address _token, uint _tokenVersion); - event ContributionAdded(uint _bountyId, uint _contributionId, address _contributor, uint _amount); - event ContributionRefunded(uint _bountyId, uint _contributionId); - event ContributionsRefunded(uint _bountyId, address _issuer, uint [] _contributionIds); - event BountyDrained(uint _bountyId, address _issuer, uint [] _amounts); - event ActionPerformed(uint _bountyId, address _fulfiller, string _data); - event BountyFulfilled(uint _bountyId, uint _fulfillmentId, address [] _fulfillers, string _data, address _submitter); - event FulfillmentUpdated(uint _bountyId, uint _fulfillmentId, address [] _fulfillers, string _data); - event FulfillmentAccepted(uint _bountyId, uint _fulfillmentId, address _approver, uint[] _tokenAmounts); - event BountyChanged(uint _bountyId, address _changer, address [] _issuers, address [] _approvers, string _data, uint _deadline); - event BountyIssuersUpdated(uint _bountyId, address _changer, address [] _issuers); - event BountyApproversUpdated(uint _bountyId, address _changer, address [] _approvers); - event BountyDataChanged(uint _bountyId, address _changer, string _data); - event BountyDeadlineChanged(uint _bountyId, address _changer, uint _deadline); + function contribute( + address _sender, + uint _bountyId, + uint _amount + ) external payable; + /* + * Events + */ + + event BountyIssued(uint _bountyId, address _creator, address [] _issuers, address [] _approvers, string _data, uint _deadline, address _token, uint _tokenVersion); + event ContributionAdded(uint _bountyId, uint _contributionId, address _contributor, uint _amount); + event ContributionRefunded(uint _bountyId, uint _contributionId); + event ContributionsRefunded(uint _bountyId, address _issuer, uint [] _contributionIds); + event BountyDrained(uint _bountyId, address _issuer, uint [] _amounts); + event ActionPerformed(uint _bountyId, address _fulfiller, string _data); + event BountyFulfilled(uint _bountyId, uint _fulfillmentId, address [] _fulfillers, string _data, address _submitter); + event FulfillmentUpdated(uint _bountyId, uint _fulfillmentId, address [] _fulfillers, string _data); + event FulfillmentAccepted(uint _bountyId, uint _fulfillmentId, address _approver, uint[] _tokenAmounts); + event BountyChanged(uint _bountyId, address _changer, address [] _issuers, address [] _approvers, string _data, uint _deadline); + event BountyIssuersUpdated(uint _bountyId, address _changer, address [] _issuers); + event BountyApproversUpdated(uint _bountyId, address _changer, address [] _approvers); + event BountyDataChanged(uint _bountyId, address _changer, string _data); + event BountyDeadlineChanged(uint _bountyId, address _changer, uint _deadline); } diff --git a/apps/projects/test/Projects.test.js b/apps/projects/test/Projects.test.js index 8d0adac16..79931958f 100644 --- a/apps/projects/test/Projects.test.js +++ b/apps/projects/test/Projects.test.js @@ -1,4 +1,4 @@ -/* global artifacts, assert, before, beforeEach, contract, context, expect, it, web3 */ +/* global artifacts, assert, before, beforeEach, contract, context, expect, it, web3, xit */ const { assertRevert } = require('@aragon/test-helpers/assertThrow') const truffleAssert = require('truffle-assertions') @@ -13,18 +13,19 @@ const getReceipt = (receipt, event, arg) => receipt.logs.filter(l => l.event === /** Useful constants */ const repoIdString = 'MDEwOIJlcG9zaXRvcnkxNjY3MjlyMjY=' const ZERO_ADDR = '0x0000000000000000000000000000000000000000' +const ANY_ADDR = '0xffffffffffffffffffffffffffffffffffffffff' const addedRepo = receipt => web3.toAscii(receipt.logs.filter(x => x.event === 'RepoAdded')[0].args.repoId) //const addedBounties = receipt => // receipt.logs.filter(x => x.event === 'BountyAdded')[2] const addedBountyInfo = receipt => - receipt.logs.filter(x => x.event === 'BountyAdded').map(event => event.args) + receipt.logs.filter(x => x.event === 'IssueUpdated').map(event => event.args) //const fulfilledBounty = receipt => // receipt.logs.filter(x => x.event === 'BountyFulfilled')[0].args contract('Projects App', accounts => { - let APP_MANAGER_ROLE, ADD_REPO_ROLE, CHANGE_SETTINGS_ROLE, CURATE_ISSUES_ROLE + let APP_MANAGER_ROLE, ADD_REPO_ROLE, CHANGE_SETTINGS_ROLE, CURATE_ISSUES_ROLE, CREATE_ISSUES_ROLE let FUND_ISSUES_ROLE, FUND_OPEN_ISSUES_ROLE, REMOVE_ISSUES_ROLE, REMOVE_REPO_ROLE let REVIEW_APPLICATION_ROLE, TRANSFER_ROLE, UPDATE_BOUNTIES_ROLE, WORK_REVIEW_ROLE let daoFact, alternateBounties, bounties, bountiesEvents, app, vaultBase, vault @@ -51,6 +52,7 @@ contract('Projects App', accounts => { APP_MANAGER_ROLE = await kernelBase.APP_MANAGER_ROLE() ADD_REPO_ROLE = await appBase.ADD_REPO_ROLE() CHANGE_SETTINGS_ROLE = await appBase.CHANGE_SETTINGS_ROLE() + CREATE_ISSUES_ROLE = await appBase.CREATE_ISSUES_ROLE() CURATE_ISSUES_ROLE = await appBase.CURATE_ISSUES_ROLE() FUND_ISSUES_ROLE = await appBase.FUND_ISSUES_ROLE() FUND_OPEN_ISSUES_ROLE = await appBase.FUND_OPEN_ISSUES_ROLE() @@ -92,6 +94,7 @@ contract('Projects App', accounts => { await acl.createPermission(root, app.address, ADD_REPO_ROLE, root) await acl.createPermission(root, app.address, CURATE_ISSUES_ROLE, root) await acl.createPermission(root, app.address, CHANGE_SETTINGS_ROLE, root) + await acl.createPermission(ANY_ADDR, app.address, CREATE_ISSUES_ROLE, root) /** Setup permission to transfer funds */ await acl.grantPermission(app.address, vault.address, TRANSFER_ROLE) @@ -136,8 +139,10 @@ contract('Projects App', accounts => { beforeEach(async () => { repoId = addedRepo( - await app.addRepo( + await app.setRepo( repoIdString, // repoId + false, + '', { from: root } ) ) @@ -174,14 +179,18 @@ contract('Projects App', accounts => { it('can remove repos', async () => { let repoId2 = addedRepo( - await app.addRepo( + await app.setRepo( 'MDawOlJlcG9zaXRvcnk3NTM5NTIyNA==', // repoId + false, + '', { from: root } ) ) let repoId3 = addedRepo( - await app.addRepo( - 'DRawOlJlcG9zaXRvcnk3NTM5NTIyNA==', // repoId + await app.setRepo( + 'DRawOlJlcG9zaXRvcnk3NTM5NTIyNA==', // repoId, + false, + '', { from: root } ) ) @@ -190,8 +199,10 @@ contract('Projects App', accounts => { assert.isTrue(await app.isRepoAdded(repoId2), 'repo2 should still be accessible') repoId3 = addedRepo( - await app.addRepo( - 'DRawOlJlcG9zaXRvcnk3NTM5NTIyNA==', // repoId + await app.setRepo( + 'DRawOlJlcG9zaXRvcnk3NTM5NTIyNA==', // repoId, + false, + '', { from: root } ) ) @@ -200,8 +211,10 @@ contract('Projects App', accounts => { assert.isTrue(await app.isRepoAdded(repoId3), 'repo3 should still be accessible') repoId2 = addedRepo( - await app.addRepo( + await app.setRepo( 'MDawOlJlcG9zaXRvcnk3NTM5NTIyNA==', // repoId + false, + '', { from: root } ) ) @@ -246,36 +259,36 @@ contract('Projects App', accounts => { const issueNumbers = issueReceipt.map(bounty => bounty.issueNumber) const issueData1 = await app.getIssue(repoId, issueNumbers[0]) assert.deepEqual( + issueData1, [ - true, new web3.BigNumber(0), - false, + new web3.BigNumber(0), new web3.BigNumber(10), - '0x0000000000000000000000000000000000000000' - ], - issueData1 + '0x0000000000000000000000000000000000000000', + 'QmbUSy8HCn8J4TMDRRdxCbK2uCCtkQyZtY6XYv3y7kLgDC' + ] ) const issueData2 = await app.getIssue(repoId, issueNumbers[1]) assert.deepEqual( + issueData2, [ - true, new web3.BigNumber(1), - false, + new web3.BigNumber(0), new web3.BigNumber(20), - '0x0000000000000000000000000000000000000000' - ], - issueData2 + '0x0000000000000000000000000000000000000000', + 'QmVtYjNij3KeyGmcgg7yVXWskLaBtov3UYL9pgcGK3MCWu' + ] ) const issueData3 = await app.getIssue(repoId, issueNumbers[2]) assert.deepEqual( + issueData3, [ - true, new web3.BigNumber(2), - false, + new web3.BigNumber(0), new web3.BigNumber(30), - '0x0000000000000000000000000000000000000000' - ], - issueData3 + '0x0000000000000000000000000000000000000000', + 'QmR45FmbVVrixReBwJkhEKde2qwHYaQzGxu4ZoDeswuF9w' + ] ) }) @@ -359,7 +372,7 @@ contract('Projects App', accounts => { ) const issue = await app.getIssue(repoId, 1) - assert.strictEqual(issue[4], root, 'assignee address incorrect') + assert.strictEqual(issue[3], root, 'assignee address incorrect') }) it('approve and reject assignment request', async () => { @@ -421,7 +434,7 @@ contract('Projects App', accounts => { }) it('work can be rejected', async () => { - const bountyId = (await app.getIssue(repoId, issueNumber))[1].toString() + const bountyId = (await app.getIssue(repoId, issueNumber))[0].toString() //console.log(bountyId) await app.requestAssignment( repoId, @@ -479,7 +492,7 @@ contract('Projects App', accounts => { true, { from: bountyManager } ) - const bountyId = (await app.getIssue(repoId, issueNumber))[1].toString() + const bountyId = (await app.getIssue(repoId, issueNumber))[0].toString() //console.log(bountyId) await bountiesEvents.fulfillBounty(root, bountyId, [root], 'test') @@ -509,43 +522,6 @@ contract('Projects App', accounts => { }) }) - it('work cannot be accepted without awarding all staked tokens', async () => { - await app.requestAssignment( - repoId, - issueNumber, - 'QmbUSy8HCn8J4TMDRRdxCbK2uCCtkQyZtY6XYv3y7kLgDd', - { from: root } - ) - const applicantQty = await app.getApplicantsLength(repoId, 1) - const applicant = await app.getApplicant( - repoId, - issueNumber, - applicantQty.toNumber() - 1 - ) - await app.reviewApplication( - repoId, - issueNumber, - applicant[0], - 'QmbUSy8HCn8J4TMDRRdxCbK2uCCtkQyZtY6XYv3y7kLgDe', - true, - { from: bountyManager } - ) - const bountyId = (await app.getIssue(repoId, issueNumber))[1].toString() - //console.log(bountyId) - await bountiesEvents.fulfillBounty(root, bountyId, [root], 'test') - return assertRevert(async () => { - await app.reviewSubmission( - repoId, - issueNumber, - 0, - true, - 'QmbUSy8HCn8J4TMDRRdxCbK2uCCtkQyZtY6XYv3y7kLgDl', - [9], - { from: bountyManager } - ) - }) - }) - it('work cannot be accepted twice', async () => { await app.requestAssignment( repoId, @@ -567,7 +543,7 @@ contract('Projects App', accounts => { true, { from: bountyManager } ) - const bountyId = (await app.getIssue(repoId, issueNumber))[1].toString() + const bountyId = (await app.getIssue(repoId, issueNumber))[0].toString() //console.log(bountyId) await bountiesEvents.fulfillBounty(root, bountyId, [root], 'test') @@ -784,7 +760,7 @@ contract('Projects App', accounts => { true, { from: bountyManager } ) - const bountyId = (await app.getIssue(repoId, issueNumber))[1].toString() + const bountyId = (await app.getIssue(repoId, issueNumber))[0].toString() await bountiesEvents.fulfillBounty(root, bountyId, [root], 'test') await app.reviewSubmission( @@ -872,6 +848,46 @@ contract('Projects App', accounts => { await app.removeRepo(repoId, { from: repoRemover }) }) }) + + it('work can be accepted multiple times', async () => { + const bountyId = (await app.getIssue(repoId, 1))[0].toString() + //console.log(bountyId) + await bountiesEvents.contribute(root, bountyId, 5, { value: 5 }) + await bountiesEvents.fulfillBounty(root, bountyId, [root], 'test') + await bountiesEvents.fulfillBounty(bountyManager, bountyId, [bountyManager], 'test2', { from: bountyManager }) + await app.reviewSubmission( + repoId, + 1, + 0, + true, + 'QmbUSy8HCn8J4TMDRRdxCbK2uCCtkQyZtY6XYv3y7kLgDl', + [9], + { from: bountyManager } + ) + + await app.reviewSubmission( + repoId, + 1, + 1, + true, + 'QmbUSy8HCn8J4TMDRRdxCbK2uCCtkQyZtY6XYv3y7kLgDl', + [6], + { from: bountyManager } + ) + + const bounty1 = await app.getIssue(repoId, 1) + assert.deepEqual( + bounty1, + [ + new web3.BigNumber(56), + new web3.BigNumber(15), + new web3.BigNumber(0), + '0xffffffffffffffffffffffffffffffffffffffff', + 'QmbUSy8HCn8J4TMDRRdxCbK2uCCtkQyZtY6XYv3y7kLgDC' + ] + ) + + }) }) context('bounty killing', async () => { @@ -890,8 +906,8 @@ contract('Projects App', accounts => { { from: bountyManager, value: 10 } ) const liveIssue = await app.getIssue(repoId, issueNumber) - let hasBounty = liveIssue[0] - assert.isTrue(hasBounty) + let liveBountySize = liveIssue[2] + assert.equal(10, liveBountySize.toNumber()) await app.removeBounties( [repoId], [issueNumber], @@ -899,10 +915,8 @@ contract('Projects App', accounts => { { from: bountyManager } ) const deadIssue = await app.getIssue(repoId, issueNumber) - hasBounty = deadIssue[0] - assert.isFalse(hasBounty) - const bountySize = deadIssue[3] - assert.equal(bountySize, 0) + const bountySize = deadIssue[2] + assert.equal(0, bountySize.toNumber()) //assert(false, 'log events') }) @@ -921,8 +935,8 @@ contract('Projects App', accounts => { { from: bountyManager, value: 10 } ) const liveIssue = await app.getIssue(repoId, issueNumber) - let hasBounty = liveIssue[0] - assert.isTrue(hasBounty) + let bountySize = liveIssue[2] + assert.equal(10, bountySize) await app.removeBounties( [repoId], [issueNumber], @@ -964,8 +978,8 @@ contract('Projects App', accounts => { ) ) const liveIssue = await app.getIssue(repoId, issueNumber) - let hasBounty = liveIssue[0] - assert.isTrue(hasBounty) + let bountySize = liveIssue[2] + assert.equal(5, bountySize) await app.removeBounties( [repoId], [issueNumber], @@ -1084,7 +1098,7 @@ contract('Projects App', accounts => { true, { from: bountyManager } ) - const bountyId = (await app.getIssue(repoId, issueNumber))[1].toString() + const bountyId = (await app.getIssue(repoId, issueNumber))[0].toString() //console.log(bountyId) await bountiesEvents.fulfillBounty(root, bountyId, [root], 'test') @@ -1156,6 +1170,122 @@ contract('Projects App', accounts => { }) }) + context('decoupled project', () => { + let repoId + beforeEach(async () => { + await app.setRepo('1', true, 'abc') + repoId = '1' + }) + it('can add a decoupled Project', async () => { + const projectInfo = await app.getRepo('1') + + assert.deepEqual( + projectInfo, + [ + new web3.BigNumber(0), + new web3.BigNumber(0), + true, + 'abc' + ] + ) + }) + it('can remove a decoupled Project', async () => { + await app.removeRepo('1', { from: repoRemover }) + await truffleAssert.fails( + app.getRepo('1'), + truffleAssert.ErrorType.REVERT, + //'REPO_NOT_ADDED' + ) + }) + + it('can log and update issues for the project', async () => { + await app.setIssue( + repoId, + 1, + 'doesn\'t work' + ) + + let issue = await app.getIssue(repoId, 1) + assert.deepEqual( + issue, + [ + new web3.BigNumber(0), + new web3.BigNumber(0), + new web3.BigNumber(0), + '0x0000000000000000000000000000000000000000', + 'doesn\'t work' + ] + ) + + await app.setIssue( + repoId, + 1, + 'doesn\'t work; let\'s get this fixed' + ) + + issue = await app.getIssue(repoId, 1) + assert.deepEqual( + issue, + [ + new web3.BigNumber(0), + new web3.BigNumber(0), + new web3.BigNumber(0), + '0x0000000000000000000000000000000000000000', + 'doesn\'t work; let\'s get this fixed' + ] + ) + }) + + + it('can log bounty against existing issue', async () => { + await app.setIssue( + repoId, + 1, + 'doesn\'t work; let\'s get this fixed' + ) + await app.addBounties( + [repoId], + [1], + [1], + [Date.now() + 86400], + [0], + [0], + 'QmbUSy8HCn8J4TMDRRdxCbK2uCCtkQyZtY6XYv3y7kLgDC', + 'adding project to existing issue', + { from: bountyManager, value: 1 } + ) + + let issue = await app.getIssue(repoId, 1) + assert.deepEqual( + issue, + [ + new web3.BigNumber(66), + new web3.BigNumber(0), + new web3.BigNumber(1), + '0x0000000000000000000000000000000000000000', + 'QmbUSy8HCn8J4TMDRRdxCbK2uCCtkQyZtY6XYv3y7kLgDC' + ] + ) + }) + + it('can update projects details', async () => { + await app.setRepo('1', true, 'abc123') + + const projectInfo = await app.getRepo('1') + + assert.deepEqual( + projectInfo, + [ + new web3.BigNumber(0), + new web3.BigNumber(0), + true, + 'abc123' + ] + ) + }) + + }) + context('issue curation', () => { // TODO: We should create every permission for every test this way to speed up testing // TODO: Create an external helper function that inits acl and sets permissions @@ -1342,25 +1472,6 @@ contract('Projects App', accounts => { }) }) - it('cannot update bounties contract with contract of invalid size', async () => { - return assertRevert(async () => { - await app.changeBountySettings( - [ 100, 300, 500, 1000 ], // xp multipliers - [ - // Experience Levels - web3.fromAscii('Beginner'), - web3.fromAscii('Intermediate'), - web3.fromAscii('Advanced'), - web3.fromAscii('Expert'), - ], - 1, // baseRate - 336, // bountyDeadline - ZERO_ADDR, // bountyCurrency - app.address // bountyAllocator - ) - }) - }) - it('can update bounties contract with a new valid contract instance', async () => { await app.changeBountySettings( [ 100, 300, 500, 1000 ], // xp multipliers @@ -1380,13 +1491,6 @@ contract('Projects App', accounts => { }) context('invalid operations', () => { - it('cannot add a repo that is already present', async () => { - await app.addRepo('abc', { from: root }) - - assertRevert(async () => { - await app.addRepo('abc', { from: root }) - }) - }) it('cannot remove a repo that was never added', async () => { assertRevert(async () => { await app.removeRepo('99999', { from: repoRemover }) @@ -1394,7 +1498,7 @@ contract('Projects App', accounts => { }) it('cannot retrieve a removed Repo', async () => { const repoId = addedRepo( - await app.addRepo('abc', { from: root }) + await app.setRepo('abc', false, '', { from: root }) ) await app.removeRepo(repoId, { from: repoRemover }) // const result = await app.getRepo(repoId) From b48653b2bed39f1703b3ec6a8caff71f65b6e6a0 Mon Sep 17 00:00:00 2001 From: Kevin Siegler Date: Wed, 22 Jan 2020 17:34:50 -0600 Subject: [PATCH 2/2] fix(projects): enable radspec for external function - `fulfillBounty` radspec message now shows up in external tx panel - remove extraneous logging from deploy script - remove unneeded functions from Bounties interface --- apps/projects/contracts/Projects.sol | 40 +++++++++---------- .../migrations/2_deploy_contracts.js | 1 - 2 files changed, 19 insertions(+), 22 deletions(-) diff --git a/apps/projects/contracts/Projects.sol b/apps/projects/contracts/Projects.sol index 64f130ef4..cb34a886b 100644 --- a/apps/projects/contracts/Projects.sol +++ b/apps/projects/contracts/Projects.sol @@ -14,27 +14,6 @@ import "@aragon/os/contracts/lib/math/SafeMath.sol"; * @dev Defines a minimal interface blueprint for the StandardBounties contract */ interface Bounties { - /** - * @notice Submit a fulfillment for issue #`_bountyId` with the following info: `_data` - */ - function fulfillBounty( - address _sender, - uint _bountyId, - address[] _fulfillers, - string _data - ) external; //{} - - /** - * @notice Update fulfillment for issue #`_bountyId` with the following info: `_data` - */ - function updateFulfillment( - address _sender, - uint _bountyId, - uint _fulfillmentId, - address[] _fulfillers, - string _data - ) external; //{} - function issueBounty( address sender, address[] _issuers, @@ -588,6 +567,25 @@ contract Projects is AragonApp, DepositableStorage { } } + /** + * @notice Submit a fulfillment for bounty #`_bountyId` with the following info: `_data` + * @dev This is a noop function implemented so the client can display the radspec + * for this external contract call + * @param _sender address of the user submitting the fulfillment + * @param _bountyId Standard Bounty Identifier + * @param _fulfillers array of users who contributed to the fulfillment of the bounty + * @param _data the provided proof of fulfillment + */ + function fulfillBounty( + address _sender, + uint _bountyId, + address[] _fulfillers, + string _data + ) external + { + revert(); + } + /////////////////////// // External utility functions /////////////////////// diff --git a/shared/integrations/StandardBounties/migrations/2_deploy_contracts.js b/shared/integrations/StandardBounties/migrations/2_deploy_contracts.js index 4276624dd..71b14aa42 100644 --- a/shared/integrations/StandardBounties/migrations/2_deploy_contracts.js +++ b/shared/integrations/StandardBounties/migrations/2_deploy_contracts.js @@ -1,7 +1,6 @@ const StandardBounties = artifacts.require("../contacts/StandardBounties.sol"); module.exports = function(deployer) { - console.log('test') deployer.deploy(StandardBounties) .then(instance => { console.log(instance.address )