diff --git a/contracts/schemes/GenericSchemeMultiCall.sol b/contracts/schemes/GenericSchemeMultiCall.sol new file mode 100644 index 00000000..e4b7da2d --- /dev/null +++ b/contracts/schemes/GenericSchemeMultiCall.sol @@ -0,0 +1,197 @@ +pragma solidity 0.5.17; +pragma experimental ABIEncoderV2; + +import "@daostack/infra/contracts/votingMachines/IntVoteInterface.sol"; +import "@daostack/infra/contracts/votingMachines/ProposalExecuteInterface.sol"; +import "../votingMachines/VotingMachineCallbacks.sol"; + +/** + * @title GenericSchemeMultiCall. + * @dev A scheme for proposing and executing calls to multiple arbitrary function + * on one or multiple contracts on behalf of the organization avatar. + */ +contract GenericSchemeMultiCall is VotingMachineCallbacks, ProposalExecuteInterface { + + event NewMultiCallProposal( + address indexed _avatar, + bytes32 indexed _proposalId, + bytes[] _callData, + uint256[] _value, + string _descriptionHash, + address[] _contractsToCall + ); + + event ProposalExecuted( + address indexed _avatar, + bytes32 indexed _proposalId + ); + + event ProposalCallExecuted( + address indexed _avatar, + bytes32 indexed _proposalId, + address _contractToCall, + bool _success, + bytes _callDataReturnValue + ); + + event ProposalExecutedByVotingMachine( + address indexed _avatar, + bytes32 indexed _proposalId, + int256 _param + ); + + event ProposalDeleted(address indexed _avatar, bytes32 indexed _proposalId); + + // Details of a voting proposal: + struct MultiCallProposal { + address[] contractsToCall; + bytes[] callData; + uint256[] value; + bool exist; + bool passed; + } + + mapping(bytes32=>MultiCallProposal) public proposals; + + IntVoteInterface public votingMachine; + bytes32 public voteParams; + mapping(address=>bool) internal contractWhitelist; + address[] public whitelistedContracts; + Avatar public avatar; + + /** + * @dev initialize + * @param _avatar the avatar to mint reputation from + * @param _votingMachine the voting machines address to + * @param _voteParams voting machine parameters. + * @param _contractWhitelist the contracts the scheme is allowed to interact with + * + */ + function initialize( + Avatar _avatar, + IntVoteInterface _votingMachine, + bytes32 _voteParams, + address[] calldata _contractWhitelist + ) + external + { + require(avatar == Avatar(0), "can be called only one time"); + require(_avatar != Avatar(0), "avatar cannot be zero"); + require(_contractWhitelist.length > 0, "contractWhitelist cannot be empty"); + avatar = _avatar; + votingMachine = _votingMachine; + voteParams = _voteParams; + for(uint i = 0; i < _contractWhitelist.length; i ++) { + contractWhitelist[_contractWhitelist[i]] = true; + whitelistedContracts.push(_contractWhitelist[i]); + } + } + + /** + * @dev execution of proposals, can only be called by the voting machine in which the vote is held. + * @param _proposalId the ID of the voting in the voting machine + * @param _decision a parameter of the voting result, 1 yes and 2 is no. + * @return bool success + */ + function executeProposal(bytes32 _proposalId, int256 _decision) + external + onlyVotingMachine(_proposalId) + returns(bool) { + MultiCallProposal storage proposal = proposals[_proposalId]; + require(proposal.exist, "must be a live proposal"); + require(proposal.passed == false, "cannot execute twice"); + + if (_decision == 1) { + proposal.passed = true; + execute(_proposalId); + } else { + delete proposals[_proposalId]; + emit ProposalDeleted(address(avatar), _proposalId); + } + + emit ProposalExecutedByVotingMachine(address(avatar), _proposalId, _decision); + return true; + } + + /** + * @dev execution of proposals after it has been decided by the voting machine + * @param _proposalId the ID of the voting in the voting machine + */ + function execute(bytes32 _proposalId) public { + MultiCallProposal storage proposal = proposals[_proposalId]; + require(proposal.exist, "must be a live proposal"); + require(proposal.passed, "proposal must passed by voting machine"); + proposal.exist = false; + bytes memory genericCallReturnValue; + bool success; + Controller controller = Controller(avatar.owner()); + + for (uint i = 0; i < proposal.contractsToCall.length; i ++) { + if (proposal.contractsToCall[i] == address(controller)) { + + (IERC20 extToken, + address spender, + uint256 valueToSpend + ) = + /* solhint-disable */ + abi.decode( + proposal.callData[i], + (IERC20, address, uint256) + ); + controller.externalTokenApproval(extToken,spender,valueToSpend,avatar); + } else { + (success, genericCallReturnValue) = + controller.genericCall(proposal.contractsToCall[i], proposal.callData[i], avatar, proposal.value[i]); + } + + emit ProposalCallExecuted(address(avatar), _proposalId, proposal.contractsToCall[i], success, genericCallReturnValue); + } + + delete proposals[_proposalId]; + emit ProposalDeleted(address(avatar), _proposalId); + emit ProposalExecuted(address(avatar), _proposalId); + } + + /** + * @dev propose to call one or multiple contracts on behalf of the _avatar + * The function trigger NewMultiCallProposal event + * @param _contractsToCall the contracts to be called + * @param _callData - The abi encode data for the calls + * @param _value value(ETH) to transfer with the calls + * @param _descriptionHash proposal description hash + * @return an id which represents the proposal + */ + + function proposeCalls(address[] memory _contractsToCall, bytes[] memory _callData, uint256[] memory _value, string memory _descriptionHash) + public + returns(bytes32 proposalId) + { + require( + (_contractsToCall.length == _callData.length) && (_contractsToCall.length == _value.length), + "Wrong length of _contractsToCall, _callData or _value arrays" + ); + for (uint i = 0; i < _contractsToCall.length; i ++) { + require( + contractWhitelist[_contractsToCall[i]] || _contractsToCall[i] == avatar.owner(), + "contractToCall is not whitelisted" + ); + } + proposalId = votingMachine.propose(2, voteParams, msg.sender, address(avatar)); + + proposals[proposalId] = MultiCallProposal({ + contractsToCall: _contractsToCall, + callData: _callData, + value: _value, + exist: true, + passed: false + }); + proposalsInfo[address(votingMachine)][proposalId] = ProposalInfo({ + blockNumber:block.number, + avatar:avatar + }); + + emit NewMultiCallProposal(address(avatar), proposalId, _callData, _value, _descriptionHash, _contractsToCall); + + } + +} \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 2db2731f..16c5c25f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -460,6 +460,11 @@ "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=" }, + "assertion-error": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", + "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==" + }, "assign-symbols": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/assign-symbols/-/assign-symbols-1.0.0.tgz", @@ -4254,8 +4259,7 @@ "lodash.isequal": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", - "integrity": "sha1-QVxEePK8wwEgwizhDtMib30+GOA=", - "dev": true + "integrity": "sha1-QVxEePK8wwEgwizhDtMib30+GOA=" }, "lodash.merge": { "version": "4.6.2", @@ -6328,6 +6332,15 @@ "original-require": "1.0.1" } }, + "truffle-assertions": { + "version": "0.9.2", + "resolved": "https://registry.npmjs.org/truffle-assertions/-/truffle-assertions-0.9.2.tgz", + "integrity": "sha512-9g2RhaxU2F8DeWhqoGQvL/bV8QVoSnQ6PY+ZPvYRP5eF7+/8LExb4mjLx/FeliLTjc3Tv1SABG05Gu5qQ/ErmA==", + "requires": { + "assertion-error": "^1.1.0", + "lodash.isequal": "^4.5.0" + } + }, "truffle-flattener": { "version": "1.4.4", "resolved": "https://registry.npmjs.org/truffle-flattener/-/truffle-flattener-1.4.4.tgz", diff --git a/test/genericscheme.js b/test/genericscheme.js index 28576408..3f7fa11d 100644 --- a/test/genericscheme.js +++ b/test/genericscheme.js @@ -232,7 +232,7 @@ contract('GenericScheme', function(accounts) { }); }); - it("Wallet - execute proposeVote -positive decision - check action - with GenesisProtocol", async function() { + it.only("Wallet - execute proposeVote -positive decision - check action - with GenesisProtocol", async function() { var wallet =await Wallet.new(); await web3.eth.sendTransaction({from:accounts[0],to:wallet.address, value: web3.utils.toWei('1', "ether")}); var standardTokenMock = await ERC20Mock.new(accounts[0],1000); diff --git a/test/genericschememulticall.js b/test/genericschememulticall.js new file mode 100644 index 00000000..0d961952 --- /dev/null +++ b/test/genericschememulticall.js @@ -0,0 +1,362 @@ +import * as helpers from './helpers'; +const constants = require('./constants'); +const GenericSchemeMultiCall = artifacts.require('./GenericSchemeMultiCall.sol'); +const DaoCreator = artifacts.require("./DaoCreator.sol"); +const ControllerCreator = artifacts.require("./ControllerCreator.sol"); +const DAOTracker = artifacts.require("./DAOTracker.sol"); +const ERC20Mock = artifacts.require("./ERC20Mock.sol"); +const ActionMock = artifacts.require("./ActionMock.sol"); + +export class GenericSchemeParams { + constructor() { + } +} + +const setupGenericSchemeParams = async function( + genericScheme, + accounts, + contractWhitelist, + genesisProtocol = false, + tokenAddress = 0, + avatar + ) { + var genericSchemeParams = new GenericSchemeParams(); + if (genesisProtocol === true){ + genericSchemeParams.votingMachine = await helpers.setupGenesisProtocol(accounts,tokenAddress,0,helpers.NULL_ADDRESS); + await genericScheme.initialize( + avatar.address, + genericSchemeParams.votingMachine.genesisProtocol.address, + genericSchemeParams.votingMachine.params, + contractWhitelist); + } + else { + genericSchemeParams.votingMachine = await helpers.setupAbsoluteVote(helpers.NULL_ADDRESS,50,genericScheme.address); + await genericScheme.initialize( + avatar.address, + genericSchemeParams.votingMachine.absoluteVote.address, + genericSchemeParams.votingMachine.params, + contractWhitelist); + } + return genericSchemeParams; +}; + +const setup = async function (accounts,contractsWhitelist,reputationAccount=0,genesisProtocol = false,tokenAddress=0) { + var testSetup = new helpers.TestSetup(); + testSetup.standardTokenMock = await ERC20Mock.new(accounts[1],100); + testSetup.GenericSchemeMultiCall = await GenericSchemeMultiCall.new(); + var controllerCreator = await ControllerCreator.new({gas: constants.ARC_GAS_LIMIT}); + var daoTracker = await DAOTracker.new({gas: constants.ARC_GAS_LIMIT}); + testSetup.daoCreator = await DaoCreator.new(controllerCreator.address,daoTracker.address,{gas:constants.ARC_GAS_LIMIT}); + testSetup.reputationArray = [20,10,70]; + if (reputationAccount === 0) { + testSetup.org = await helpers.setupOrganizationWithArrays(testSetup.daoCreator,[accounts[0],accounts[1],accounts[2]],[1000,1000,1000],testSetup.reputationArray); + } else { + testSetup.org = await helpers.setupOrganizationWithArrays(testSetup.daoCreator,[accounts[0],accounts[1],reputationAccount],[1000,1000,1000],testSetup.reputationArray); + } + testSetup.genericSchemeParams= await setupGenericSchemeParams(testSetup.GenericSchemeMultiCall,accounts,contractsWhitelist,genesisProtocol,tokenAddress,testSetup.org.avatar); + var permissions = "0x00000010"; + + + await testSetup.daoCreator.setSchemes(testSetup.org.avatar.address, + [testSetup.GenericSchemeMultiCall.address], + [helpers.NULL_HASH],[permissions],"metaData"); + + return testSetup; +}; + +const createCallToActionMock = async function(_avatar,_actionMock) { + return await new web3.eth.Contract(_actionMock.abi).methods.test2(_avatar).encodeABI(); +}; + +contract('GenericSchemeMultiCall', function(accounts) { + before(function() { + helpers.etherForEveryone(accounts); + }); + + it("proposeCall log", async function() { + var actionMock =await ActionMock.new(); + var testSetup = await setup(accounts,[actionMock.address]); + var callData = await createCallToActionMock(testSetup.org.avatar.address,actionMock); + var tx = await testSetup.GenericSchemeMultiCall.proposeCalls( + [actionMock.address],[callData],[0],helpers.NULL_HASH); + assert.equal(tx.logs.length, 1); + assert.equal(tx.logs[0].event, "NewMultiCallProposal"); + }); + + it("proposeCall log - with invalid array - reverts", async function() { + var actionMock =await ActionMock.new(); + var testSetup = await setup(accounts,[actionMock.address]); + var callData = await createCallToActionMock(testSetup.org.avatar.address,actionMock); + try { + await testSetup.GenericSchemeMultiCall.proposeCalls( + [actionMock.address,actionMock.address],[callData],[0],helpers.NULL_HASH); + assert(false, "Wrong length of _contractsToCall, _callData or _value arrays"); + } catch(error) { + helpers.assertVMException(error); + } + try { + await testSetup.GenericSchemeMultiCall.proposeCalls( + [actionMock.address,actionMock.address],[callData,callData],[0],helpers.NULL_HASH); + assert(false, "Wrong length of _contractsToCall, _callData or _value arrays"); + } catch(error) { + helpers.assertVMException(error); + } + try { + await testSetup.GenericSchemeMultiCall.proposeCalls( + [actionMock.address,actionMock.address],[callData],[0,0],helpers.NULL_HASH); + assert(false, "Wrong length of _contractsToCall, _callData or _value arrays"); + } catch(error) { + helpers.assertVMException(error); + } + }); + + it("execute proposeCall -no decision - proposal data delete", async function() { + var actionMock =await ActionMock.new(); + var testSetup = await setup(accounts,[actionMock.address]); + var callData = await createCallToActionMock(testSetup.org.avatar.address,actionMock); + var tx = await testSetup.GenericSchemeMultiCall.proposeCalls( + [actionMock.address],[callData],[0],helpers.NULL_HASH); + var proposalId = await helpers.getValueFromLogs(tx, '_proposalId'); + await testSetup.genericSchemeParams.votingMachine.absoluteVote.vote(proposalId,0,0,helpers.NULL_ADDRESS,{from:accounts[2]}); + //check organizationsProposals after execution + var proposal = await testSetup.GenericSchemeMultiCall.proposals(proposalId); + assert.equal(proposal.passed,false); + assert.equal(proposal.callData,null); + }); + + it("execute proposeVote -positive decision - proposal data delete", async function() { + var actionMock =await ActionMock.new(); + var testSetup = await setup(accounts,[actionMock.address]); + var callData = await createCallToActionMock(testSetup.org.avatar.address,actionMock); + var tx = await testSetup.GenericSchemeMultiCall.proposeCalls( + [actionMock.address],[callData],[0],helpers.NULL_HASH); + var proposalId = await helpers.getValueFromLogs(tx, '_proposalId'); + var proposal = await testSetup.GenericSchemeMultiCall.proposals(proposalId); + await testSetup.genericSchemeParams.votingMachine.absoluteVote.vote(proposalId,1,0,helpers.NULL_ADDRESS,{from:accounts[2]}); + //check organizationsProposals after execution + proposal = await testSetup.GenericSchemeMultiCall.proposals(proposalId); + assert.equal(proposal.callData,null);//new contract address + }); + + it("execute proposeVote -positive decision - destination reverts", async function() { + var actionMock =await ActionMock.new(); + var testSetup = await setup(accounts,[actionMock.address]); + var callData = await createCallToActionMock(helpers.NULL_ADDRESS,actionMock); + var tx = await testSetup.GenericSchemeMultiCall.proposeCalls( + [actionMock.address],[callData],[0],helpers.NULL_HASH); + var proposalId = await helpers.getValueFromLogs(tx, '_proposalId'); + //actionMock revert because msg.sender is not the _addr param at actionMock though the whole proposal execution will fail. + await testSetup.genericSchemeParams.votingMachine.absoluteVote.vote(proposalId,1,0,helpers.NULL_ADDRESS,{from:accounts[2]}); + await testSetup.GenericSchemeMultiCall.getPastEvents('ProposalCallExecuted', { + fromBlock: tx.blockNumber, + toBlock: 'latest' + }) + .then(function(events){ + assert.equal(events[0].event,"ProposalCallExecuted"); + assert.equal(events[0].args._success,false); + }); + }); + + it("execute proposeVote -positive decision - not whitelisted contract", async function() { + var actionMock =await ActionMock.new(); + var testSetup = await setup(accounts,[accounts[1]]); + var callData = await createCallToActionMock(helpers.NULL_ADDRESS,actionMock); + try { + await testSetup.GenericSchemeMultiCall.proposeCalls( + [actionMock.address],[callData],[0],helpers.NULL_HASH); + assert(false, "contractToCall is not whitelisted"); + } catch(error) { + helpers.assertVMException(error); + } + }); + + it("execute proposeVote without return value-positive decision - check action", async function() { + var actionMock =await ActionMock.new(); + var testSetup = await setup(accounts,[actionMock.address]); + const encodeABI = await new web3.eth.Contract(actionMock.abi).methods.withoutReturnValue(testSetup.org.avatar.address).encodeABI(); + var tx = await testSetup.GenericSchemeMultiCall.proposeCalls([actionMock.address],[encodeABI],[0],helpers.NULL_HASH); + var proposalId = await helpers.getValueFromLogs(tx, '_proposalId'); + await testSetup.genericSchemeParams.votingMachine.absoluteVote.vote(proposalId,1,0,helpers.NULL_ADDRESS,{from:accounts[2]}); + }); + + it("execute should fail if not executed from votingMachine", async function() { + var actionMock =await ActionMock.new(); + var testSetup = await setup(accounts,[actionMock.address]); + const encodeABI = await new web3.eth.Contract(actionMock.abi).methods.withoutReturnValue(testSetup.org.avatar.address).encodeABI(); + var tx = await testSetup.GenericSchemeMultiCall.proposeCalls([actionMock.address],[encodeABI],[0],helpers.NULL_HASH); + var proposalId = await helpers.getValueFromLogs(tx, '_proposalId'); + try { + await testSetup.GenericSchemeMultiCall.execute( proposalId); + assert(false, "execute should fail if not executed from votingMachine"); + } catch(error) { + helpers.assertVMException(error); + } + + }); + + it("execute proposeVote -positive decision - check action - with GenesisProtocol", async function() { + var actionMock =await ActionMock.new(); + var standardTokenMock = await ERC20Mock.new(accounts[0],1000); + var testSetup = await setup(accounts,[actionMock.address],0,true,standardTokenMock.address); + var value = 123; + var callData = await createCallToActionMock(testSetup.org.avatar.address,actionMock); + var tx = await testSetup.GenericSchemeMultiCall.proposeCalls([actionMock.address],[callData],[value],helpers.NULL_HASH); + var proposalId = await helpers.getValueFromLogs(tx, '_proposalId'); + //transfer some eth to avatar + await web3.eth.sendTransaction({from:accounts[0],to:testSetup.org.avatar.address, value: web3.utils.toWei('1', "ether")}); + assert.equal(await web3.eth.getBalance(actionMock.address),0); + tx = await testSetup.genericSchemeParams.votingMachine.genesisProtocol.vote(proposalId,1,0,helpers.NULL_ADDRESS,{from:accounts[2]}); + await testSetup.GenericSchemeMultiCall.getPastEvents('ProposalExecutedByVotingMachine', { + fromBlock: tx.blockNumber, + toBlock: 'latest' + }) + .then(function(events){ + assert.equal(events[0].event,"ProposalExecutedByVotingMachine"); + assert.equal(events[0].args._param,1); + }); + assert.equal(await web3.eth.getBalance(actionMock.address),value); + }); + + it("execute proposeVote -negative decision - check action - with GenesisProtocol", async function() { + var actionMock =await ActionMock.new(); + var standardTokenMock = await ERC20Mock.new(accounts[0],1000); + var testSetup = await setup(accounts,[actionMock.address],0,true,standardTokenMock.address); + + var callData = await createCallToActionMock(testSetup.org.avatar.address,actionMock); + var tx = await testSetup.GenericSchemeMultiCall.proposeCalls([actionMock.address],[callData],[0],helpers.NULL_HASH); + var proposalId = await helpers.getValueFromLogs(tx, '_proposalId'); + tx = await testSetup.genericSchemeParams.votingMachine.genesisProtocol.vote(proposalId,2,0,helpers.NULL_ADDRESS,{from:accounts[2]}); + await testSetup.GenericSchemeMultiCall.getPastEvents('ProposalExecutedByVotingMachine', { + fromBlock: tx.blockNumber, + toBlock: 'latest' + }) + .then(function(events){ + assert.equal(events[0].event,"ProposalExecutedByVotingMachine"); + assert.equal(events[0].args._param,2); + }); + }); + + it("execute proposeVote with multiple calls -positive decision - check action - with GenesisProtocol", async function() { + var actionMock =await ActionMock.new(); + var actionMock2 =await ActionMock.new(); + var standardTokenMock = await ERC20Mock.new(accounts[0],1000); + var testSetup = await setup(accounts,[actionMock.address,actionMock2.address],0,true,standardTokenMock.address); + + var callData = await createCallToActionMock(testSetup.org.avatar.address,actionMock); + var callData2 = await createCallToActionMock(testSetup.org.avatar.address,actionMock); + var tx = await testSetup.GenericSchemeMultiCall.proposeCalls([actionMock.address,actionMock2.address],[callData,callData2],[0,0],helpers.NULL_HASH); + var proposalId = await helpers.getValueFromLogs(tx, '_proposalId'); + tx = await testSetup.genericSchemeParams.votingMachine.genesisProtocol.vote(proposalId,1,0,helpers.NULL_ADDRESS,{from:accounts[2]}); + await testSetup.GenericSchemeMultiCall.getPastEvents('ProposalExecutedByVotingMachine', { + fromBlock: tx.blockNumber, + toBlock: 'latest' + }) + .then(function(events){ + assert.equal(events[0].event,"ProposalExecutedByVotingMachine"); + assert.equal(events[0].args._param,1); + }); + }); + + it("execute proposeVote with multiple calls -positive decision - one failed transaction", async function() { + var actionMock =await ActionMock.new(); + var actionMock2 =await ActionMock.new(); + var standardTokenMock = await ERC20Mock.new(accounts[0],1000); + var testSetup = await setup(accounts,[actionMock.address,actionMock2.address],0,true,standardTokenMock.address); + var callData = await createCallToActionMock(testSetup.org.avatar.address,actionMock); + var callData2 = await createCallToActionMock(accounts[0],actionMock); + var tx = await testSetup.GenericSchemeMultiCall.proposeCalls([actionMock.address,actionMock2.address],[callData,callData2],[0,0],helpers.NULL_HASH); + var proposalId = await helpers.getValueFromLogs(tx, '_proposalId'); + var proposal = await testSetup.GenericSchemeMultiCall.proposals(proposalId); + assert.equal(proposal.exist,true); + assert.equal(proposal.passed,false); + await testSetup.genericSchemeParams.votingMachine.genesisProtocol.vote(proposalId,1,0,helpers.NULL_ADDRESS,{from:accounts[2]}); + await testSetup.GenericSchemeMultiCall.getPastEvents('ProposalCallExecuted', { + fromBlock: tx.blockNumber, + toBlock: 'latest' + }) + .then(function(events){ + assert.equal(events[0].event,"ProposalCallExecuted"); + assert.equal(events[0].args._success,true); + assert.equal(events[1].event,"ProposalCallExecuted"); + assert.equal(events[1].args._success,false); + }); + }); + + it("execute proposeVote with multiple calls with votingMachine -positive decision", async function() { + var actionMock =await ActionMock.new(); + var standardTokenMock = await ERC20Mock.new(accounts[0],1000); + var testSetup = await setup(accounts,[actionMock.address],0,true,standardTokenMock.address); + var avatarInst = await new web3.eth.Contract(testSetup.org.avatar.abi,testSetup.org.avatar.address); + var controllerAddr = await avatarInst.methods.owner().call() + var encodedTokenApproval= await web3.eth.abi.encodeParameters(['address','address', 'uint256'], [standardTokenMock.address, accounts[3], 1000]); + var callData = await createCallToActionMock(testSetup.org.avatar.address,actionMock); + var tx = await testSetup.GenericSchemeMultiCall.proposeCalls([actionMock.address,controllerAddr],[callData,encodedTokenApproval],[0,0],helpers.NULL_HASH); + var proposalId = await helpers.getValueFromLogs(tx, '_proposalId'); + var proposal = await testSetup.GenericSchemeMultiCall.proposals(proposalId); + assert.equal(proposal.exist,true); + assert.equal(proposal.passed,false); + await testSetup.genericSchemeParams.votingMachine.genesisProtocol.vote(proposalId,1,0,helpers.NULL_ADDRESS,{from:accounts[2]}); + await testSetup.GenericSchemeMultiCall.getPastEvents('ProposalCallExecuted', { + fromBlock: tx.blockNumber, + toBlock: 'latest' + }) + .then(function(events){ + assert.equal(events[0].event,"ProposalCallExecuted"); + assert.equal(events[0].args._success,true); + assert.equal(events[1].event,"ProposalCallExecuted"); + assert.equal(events[1].args._success,true); + }); + }); + + it("cannot init without contract whitelist", async function() { + var actionMock =await ActionMock.new(); + var testSetup = await setup(accounts,[actionMock.address]); + var genericSchemeMultiCall =await GenericSchemeMultiCall.new(); + + try { + await genericSchemeMultiCall.initialize( + testSetup.org.avatar.address, + accounts[0], + helpers.SOME_HASH, + [] + ); + assert(false, "contractWhitelist cannot be empty"); + } catch(error) { + helpers.assertVMException(error); + } + + }); + + it("cannot init twice", async function() { + var actionMock =await ActionMock.new(); + var testSetup = await setup(accounts,[actionMock.address]); + try { + await testSetup.GenericSchemeMultiCall.initialize( + testSetup.org.avatar.address, + accounts[0], + helpers.SOME_HASH, + [accounts[0]] + ); + assert(false, "cannot init twice"); + } catch(error) { + helpers.assertVMException(error); + } + }); + + it("can init with multiple contracts on whitelist", async function() { + var actionMock =await ActionMock.new(); + var testSetup = await setup(accounts,[actionMock.address]); + var genericSchemeMultiCall =await GenericSchemeMultiCall.new(); + await genericSchemeMultiCall.initialize( + testSetup.org.avatar.address, + accounts[0], + helpers.SOME_HASH, + [accounts[0],accounts[1],accounts[2],accounts[3]] + ); + assert.equal(await genericSchemeMultiCall.whitelistedContracts(0),accounts[0]); + assert.equal(await genericSchemeMultiCall.whitelistedContracts(1),accounts[1]); + assert.equal(await genericSchemeMultiCall.whitelistedContracts(2),accounts[2]); + assert.equal(await genericSchemeMultiCall.whitelistedContracts(3),accounts[3]); + }); + +});