From 19682c21366708dca193b885461c9918acc35738 Mon Sep 17 00:00:00 2001 From: orenyodfat Date: Wed, 14 Oct 2020 15:05:42 +0300 Subject: [PATCH] GenericSchemeMultiCall : Simplification. + Schemeconstraint (#795) * simplify contract * tests * test approval * check spender is white listed * clean * poc * Add SchemeConstraints interface * add eth constraint as an example for DxDaoSchemeConstraint * + * more .. * fix dxdao constraints * check schemeConstraint exist * + comments * coverage * remove unused var * more tests * Bump version Co-authored-by: benk10 --- contracts/schemes/DxDaoSchemeConstraints.sol | 148 ++++++++++ contracts/schemes/GenericSchemeMultiCall.sol | 72 ++--- contracts/schemes/SchemeConstraints.sol | 44 +++ package-lock.json | 2 +- package.json | 2 +- test/genericschememulticall.js | 279 +++++++++++++------ 6 files changed, 406 insertions(+), 141 deletions(-) create mode 100644 contracts/schemes/DxDaoSchemeConstraints.sol create mode 100644 contracts/schemes/SchemeConstraints.sol diff --git a/contracts/schemes/DxDaoSchemeConstraints.sol b/contracts/schemes/DxDaoSchemeConstraints.sol new file mode 100644 index 00000000..a041ffc0 --- /dev/null +++ b/contracts/schemes/DxDaoSchemeConstraints.sol @@ -0,0 +1,148 @@ +pragma solidity 0.5.17; +pragma experimental ABIEncoderV2; + +import "./SchemeConstraints.sol"; + + +contract DxDaoSchemeConstraints is SchemeConstraints { + using SafeMath for uint256; + + uint256 public initialTimestamp; + uint256 public periodSize; + uint256 public periodLimitWei; + + mapping(address=>uint256) public periodLimitToken; + mapping (uint256 => mapping(address => uint256)) public periodSpendingToken; + mapping(uint256=>uint256) public periodSpendingWei; + mapping(address=>bool) public contractsWhiteListMap; + bytes4 private constant APPROVE_SIGNATURE = 0x095ea7b3;//approve(address,uint256) + + /* @dev initialize + * @param _periodSize the time period to limit the tokens and eth spending + * @param _periodLimitWei the limit of eth which can be sent per period + * @param _periodLimitTokensAddresses tokens to limit + * @param _periodLimitTokensAmounts the limit of token which can be sent per period + * @param _contractsWhiteList the contracts the scheme is allowed to interact with + */ + function initialize( + uint256 _periodSize, + uint256 _periodLimitWei, + address[] calldata _periodLimitTokensAddresses, + uint256[] calldata _periodLimitTokensAmounts, + address[] calldata _contractsWhiteList + ) + external { + require(initialTimestamp == 0, "cannot initialize twice"); + require(_periodSize > 0, "preriod size should be greater than 0"); + require(_periodLimitTokensAddresses.length == _periodLimitTokensAmounts.length, + "invalid length _periodLimitTokensAddresses"); + periodSize = _periodSize; + periodLimitWei = _periodLimitWei; + // solhint-disable-next-line not-rely-on-time + initialTimestamp = block.timestamp; + for (uint i = 0; i < _contractsWhiteList.length; i++) { + contractsWhiteListMap[_contractsWhiteList[i]] = true; + } + for (uint i = 0; i < _periodLimitTokensAmounts.length; i++) { + periodLimitToken[_periodLimitTokensAddresses[i]] = _periodLimitTokensAmounts[i]; + } + contractsWhiteList = _contractsWhiteList; + } + + /* + * @dev isAllowedToCall should be called upon a proposal execution. + * - check that the total spending of tokens within a 'periodSize' does not exceed the periodLimit per token + * - check that the total sending of eth within a 'periodSize' does not exceed the periodLimit + * @param _contractsToCall the contracts to be called + * @param _callsData - The abi encode data for the calls + * @param _values value(ETH) to transfer with the calls + * @param _avatar avatar + * @return bool value true-allowed false not allowed + */ + function isAllowedToCall( + address[] calldata _contractsToCall, + bytes[] calldata _callsData, + uint256[] calldata _values, + Avatar + ) + external + returns(bool) + { + + uint256 observervationIndex = observationIndex(); + uint256 totalPeriodSpendingInWei; + for (uint i = 0; i < _contractsToCall.length; i++) { + // constraint eth transfer + totalPeriodSpendingInWei = totalPeriodSpendingInWei.add(_values[i]); + bytes memory callData = _callsData[i]; + // constraint approve calls + if (callData[0] == APPROVE_SIGNATURE[0] && + callData[1] == APPROVE_SIGNATURE[1] && + callData[2] == APPROVE_SIGNATURE[2] && + callData[3] == APPROVE_SIGNATURE[3]) { + uint256 amount; + address contractToCall = _contractsToCall[i]; + // solhint-disable-next-line no-inline-assembly + assembly { + amount := mload(add(callData, 68)) + } + periodSpendingToken[observervationIndex][contractToCall] = + periodSpendingToken[observervationIndex][contractToCall].add(amount); + require( + periodSpendingToken[observervationIndex][contractToCall] <= periodLimitToken[contractToCall], + "periodSpendingTokensExceeded"); + } + + } + periodSpendingWei[observervationIndex] = + periodSpendingWei[observervationIndex].add(totalPeriodSpendingInWei); + require(periodSpendingWei[observervationIndex] <= periodLimitWei, "periodSpendingWeiExceeded"); + return true; + } + + /* + * @dev isAllowedToPropose should be called upon a proposal submition. + * allow only whitelisted target contracts or 'approve' calls which the 'spender' is whitelisted + * @param _contractsToCall the contracts to be called + * @param _callsData - The abi encode data for the calls + * @param _values value(ETH) to transfer with the calls + * @param _avatar avatar + * @return bool value true-allowed false not allowed + */ + function isAllowedToPropose( + address[] calldata _contractsToCall, + bytes[] calldata _callsData, + uint256[] calldata, + Avatar) + external + returns(bool) + { + for (uint i = 0; i < _contractsToCall.length; i++) { + if (!contractsWhiteListMap[_contractsToCall[i]]) { + address spender; + bytes memory callData = _callsData[i]; + require( + callData[0] == APPROVE_SIGNATURE[0] && + callData[1] == APPROVE_SIGNATURE[1] && + callData[2] == APPROVE_SIGNATURE[2] && + callData[3] == APPROVE_SIGNATURE[3], + "allow only approve call for none whitelistedContracts"); + //in solidity > 6 this can be replaced by: + //(spender,) = abi.descode(callData[4:], (address, uint)); + // see https://github.com/ethereum/solidity/issues/9439 + // solhint-disable-next-line no-inline-assembly + assembly { + spender := mload(add(callData, 36)) + } + require(contractsWhiteListMap[spender], "spender contract not whitelisted"); + } + } + return true; + } + + function observationIndex() public view returns (uint256) { + // solhint-disable-next-line not-rely-on-time + return ((block.timestamp - initialTimestamp) / periodSize); + } + +} diff --git a/contracts/schemes/GenericSchemeMultiCall.sol b/contracts/schemes/GenericSchemeMultiCall.sol index fdde48fc..94507367 100644 --- a/contracts/schemes/GenericSchemeMultiCall.sol +++ b/contracts/schemes/GenericSchemeMultiCall.sol @@ -4,6 +4,7 @@ pragma experimental ABIEncoderV2; import "@daostack/infra/contracts/votingMachines/IntVoteInterface.sol"; import "@daostack/infra/contracts/votingMachines/ProposalExecuteInterface.sol"; import "../votingMachines/VotingMachineCallbacks.sol"; +import "./SchemeConstraints.sol"; /** @@ -24,12 +25,10 @@ contract GenericSchemeMultiCall is VotingMachineCallbacks, ProposalExecuteInterf } mapping(bytes32=>MultiCallProposal) public proposals; - IntVoteInterface public votingMachine; bytes32 public voteParams; - mapping(address=>bool) internal contractWhitelist; - address[] public whitelistedContracts; Avatar public avatar; + SchemeConstraints public schemeConstraints; event NewMultiCallProposal( address indexed _avatar, @@ -61,37 +60,26 @@ contract GenericSchemeMultiCall is VotingMachineCallbacks, ProposalExecuteInterf event ProposalDeleted(address indexed _avatar, bytes32 indexed _proposalId); - /** - * @dev initialize + /* @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 - * + * @param _schemeConstraints the schemeConstraints contracts. */ function initialize( Avatar _avatar, IntVoteInterface _votingMachine, bytes32 _voteParams, - address[] calldata _contractWhitelist + SchemeConstraints _schemeConstraints ) 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; - /* Whitelist controller by default*/ - Controller controller = Controller(_avatar.owner()); - whitelistedContracts.push(address(controller)); - contractWhitelist[address(controller)] = true; - - for (uint i = 0; i < _contractWhitelist.length; i++) { - contractWhitelist[_contractWhitelist[i]] = true; - whitelistedContracts.push(_contractWhitelist[i]); - } + schemeConstraints = _schemeConstraints; } /** @@ -127,28 +115,23 @@ contract GenericSchemeMultiCall is VotingMachineCallbacks, ProposalExecuteInterf MultiCallProposal storage proposal = proposals[_proposalId]; require(proposal.exist, "must be a live proposal"); require(proposal.passed, "proposal must passed by voting machine"); + if (schemeConstraints != SchemeConstraints(0)) { + require( + schemeConstraints.isAllowedToCall( + proposal.contractsToCall, + proposal.callsData, + proposal.values, + avatar), + "call is not allowed"); + } proposal.exist = false; bytes memory genericCallReturnValue; bool success; - Controller controller = Controller(whitelistedContracts[0]); - + Controller controller = Controller(avatar.owner()); for (uint i = 0; i < proposal.contractsToCall.length; i++) { bytes memory callData = proposal.callsData[i]; - if (proposal.contractsToCall[i] == address(controller)) { - (IERC20 extToken, - address spender, - uint256 valueToSpend - ) = - abi.decode( - callData, - (IERC20, address, uint256) - ); - success = controller.externalTokenApproval(extToken, spender, valueToSpend, avatar); - } else { - (success, genericCallReturnValue) = - controller.genericCall(proposal.contractsToCall[i], callData, avatar, proposal.values[i]); - } - + (success, genericCallReturnValue) = + controller.genericCall(proposal.contractsToCall[i], callData, avatar, proposal.values[i]); /* Whole transaction will be reverted if at least one call fails*/ require(success, "Proposal call failed"); emit ProposalCallExecuted( @@ -190,19 +173,14 @@ contract GenericSchemeMultiCall is VotingMachineCallbacks, ProposalExecuteInterf (_contractsToCall.length == _callsData.length) && (_contractsToCall.length == _values.length), "Wrong length of _contractsToCall, _callsDataLens or _values arrays" ); - for (uint i = 0; i < _contractsToCall.length; i++) { + if (schemeConstraints != SchemeConstraints(0)) { require( - contractWhitelist[_contractsToCall[i]], "contractToCall is not whitelisted" - ); - if (_contractsToCall[i] == whitelistedContracts[0]) { - - (, address spender,) = - abi.decode( - _callsData[i], - (IERC20, address, uint256) - ); - require(contractWhitelist[spender], "spender contract not whitelisted"); - } + schemeConstraints.isAllowedToPropose( + _contractsToCall, + _callsData, + _values, + avatar), + "propose is not allowed"); } proposalId = votingMachine.propose(2, voteParams, msg.sender, address(avatar)); diff --git a/contracts/schemes/SchemeConstraints.sol b/contracts/schemes/SchemeConstraints.sol new file mode 100644 index 00000000..60d5091f --- /dev/null +++ b/contracts/schemes/SchemeConstraints.sol @@ -0,0 +1,44 @@ +pragma solidity 0.5.17; +pragma experimental ABIEncoderV2; +import "../controller/Avatar.sol"; + + +contract SchemeConstraints { + + address[] public contractsWhiteList; + + /* + * @dev isAllowedToCall should be called upon a proposal execution. + * @param _contractsToCall the contracts to be called + * @param _callsData - The abi encode data for the calls + * @param _values value(ETH) to transfer with the calls + * @param _avatar avatar + * @return bool value true-allowed false not allowed + */ + function isAllowedToCall( + address[] calldata _contractsToCall, + bytes[] calldata _callsData, + uint256[] calldata _values, + Avatar _avatar) + external returns(bool); + + /* + * @dev isAllowedToPropose should be called upon a proposal submition. + * @param _contractsToCall the contracts to be called + * @param _callsData - The abi encode data for the calls + * @param _values value(ETH) to transfer with the calls + * @param _avatar avatar + * @return bool value true-allowed false not allowed + */ + function isAllowedToPropose( + address[] calldata _contractsToCall, + bytes[] calldata _callsData, + uint256[] calldata _values, + Avatar _avatar) + external returns(bool); + + function getContractsWhiteList() external view returns(address[] memory) { + return contractsWhiteList; + } + +} diff --git a/package-lock.json b/package-lock.json index 9ce12076..632c8d1c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "@daostack/arc", - "version": "0.0.1-rc.45", + "version": "0.0.1-rc.46", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 173b2b13..bfd55323 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@daostack/arc", - "version": "0.0.1-rc.45", + "version": "0.0.1-rc.46", "description": "A platform for building DAOs", "files": [ "contracts/", diff --git a/test/genericschememulticall.js b/test/genericschememulticall.js index 516acbd3..f389c698 100644 --- a/test/genericschememulticall.js +++ b/test/genericschememulticall.js @@ -6,6 +6,7 @@ const ControllerCreator = artifacts.require("./ControllerCreator.sol"); const DAOTracker = artifacts.require("./DAOTracker.sol"); const ERC20Mock = artifacts.require("./ERC20Mock.sol"); const ActionMock = artifacts.require("./ActionMock.sol"); +const DxDaoSchemeConstraints = artifacts.require("./DxDaoSchemeConstraints.sol"); export class GenericSchemeParams { constructor() { @@ -15,10 +16,10 @@ export class GenericSchemeParams { const setupGenericSchemeParams = async function( genericScheme, accounts, - contractWhitelist, genesisProtocol = false, tokenAddress = 0, - avatar + avatar, + schemeConstraintsAddress ) { var genericSchemeParams = new GenericSchemeParams(); if (genesisProtocol === true){ @@ -27,7 +28,7 @@ const setupGenericSchemeParams = async function( avatar.address, genericSchemeParams.votingMachine.genesisProtocol.address, genericSchemeParams.votingMachine.params, - contractWhitelist); + schemeConstraintsAddress); } else { genericSchemeParams.votingMachine = await helpers.setupAbsoluteVote(helpers.NULL_ADDRESS,50,genericScheme.address); @@ -35,15 +36,20 @@ const setupGenericSchemeParams = async function( avatar.address, genericSchemeParams.votingMachine.absoluteVote.address, genericSchemeParams.votingMachine.params, - contractWhitelist); + schemeConstraintsAddress); } return genericSchemeParams; }; -const setup = async function (accounts,contractsWhitelist,reputationAccount=0,genesisProtocol = false,tokenAddress=0) { +const setup = async function (accounts, + contractsWhiteList, + reputationAccount=0, + genesisProtocol = false, + tokenAddress=helpers.NULL_ADDRESS, + useSchemeConstraint = true) { var testSetup = new helpers.TestSetup(); testSetup.standardTokenMock = await ERC20Mock.new(accounts[1],100); - testSetup.GenericSchemeMultiCall = await GenericSchemeMultiCall.new(); + testSetup.genericSchemeMultiCall = await GenericSchemeMultiCall.new(); var controllerCreator = await ControllerCreator.new(); var daoTracker = await DAOTracker.new(); testSetup.daoCreator = await DaoCreator.new(controllerCreator.address,daoTracker.address); @@ -53,12 +59,20 @@ const setup = async function (accounts,contractsWhitelist,reputationAccount=0,ge } 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 schemeConstraintsAddress; + if (useSchemeConstraint) { + testSetup.schemeConstraints = await DxDaoSchemeConstraints.new(); + schemeConstraintsAddress = testSetup.schemeConstraints.address; + await testSetup.schemeConstraints.initialize(100000,100000,[tokenAddress],[1000],contractsWhiteList); + } else { + schemeConstraintsAddress = helpers.NULL_ADDRESS; + } + testSetup.genericSchemeParams= await setupGenericSchemeParams(testSetup.genericSchemeMultiCall,accounts,genesisProtocol,tokenAddress,testSetup.org.avatar,schemeConstraintsAddress); var permissions = "0x00000010"; await testSetup.daoCreator.setSchemes(testSetup.org.avatar.address, - [testSetup.GenericSchemeMultiCall.address], + [testSetup.genericSchemeMultiCall.address], [helpers.NULL_HASH],[permissions],"metaData"); return testSetup; @@ -68,6 +82,10 @@ const createCallToActionMock = async function(_avatar,_actionMock) { return await new web3.eth.Contract(_actionMock.abi).methods.test2(_avatar).encodeABI(); }; +const createCallToTokenApproval = async function(_token,_spender,_amount) { + return await new web3.eth.Contract(_token.abi).methods.approve(_spender,_amount).encodeABI(); +}; + contract('GenericSchemeMultiCall', function(accounts) { before(function() { helpers.etherForEveryone(accounts); @@ -77,7 +95,7 @@ contract('GenericSchemeMultiCall', function(accounts) { 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( + var tx = await testSetup.genericSchemeMultiCall.proposeCalls( [actionMock.address],[callData],[10],"description"); assert.equal(tx.logs.length, 1); assert.equal(tx.logs[0].event, "NewMultiCallProposal"); @@ -92,21 +110,21 @@ contract('GenericSchemeMultiCall', function(accounts) { var testSetup = await setup(accounts,[actionMock.address]); var callData = await createCallToActionMock(testSetup.org.avatar.address,actionMock); try { - await testSetup.GenericSchemeMultiCall.proposeCalls( + await testSetup.genericSchemeMultiCall.proposeCalls( [actionMock.address,actionMock.address],[callData],[0],helpers.NULL_HASH); assert(false, "Wrong length of _contractsToCall, _callsDataLens or _value arrays"); } catch(error) { helpers.assertVMException(error); } try { - await testSetup.GenericSchemeMultiCall.proposeCalls( + await testSetup.genericSchemeMultiCall.proposeCalls( [actionMock.address,actionMock.address],[callData],[0],helpers.NULL_HASH); assert(false, "Wrong length of _contractsToCall, _callsDataLens or _value arrays"); } catch(error) { helpers.assertVMException(error); } try { - await testSetup.GenericSchemeMultiCall.proposeCalls( + await testSetup.genericSchemeMultiCall.proposeCalls( [actionMock.address,actionMock.address],[callData],[0,0],helpers.NULL_HASH); assert(false, "Wrong length of _contractsToCall, _callsDataLens or _value arrays"); } catch(error) { @@ -118,12 +136,12 @@ contract('GenericSchemeMultiCall', function(accounts) { 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( + 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); + var proposal = await testSetup.genericSchemeMultiCall.proposals(proposalId); assert.equal(proposal.passed,false); assert.equal(proposal.callData,null); }); @@ -132,13 +150,13 @@ contract('GenericSchemeMultiCall', function(accounts) { 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( + 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); + 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); + proposal = await testSetup.genericSchemeMultiCall.proposals(proposalId); assert.equal(proposal.callData,null);//new contract address }); @@ -146,13 +164,13 @@ contract('GenericSchemeMultiCall', function(accounts) { 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( + 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]}); try { - await testSetup.GenericSchemeMultiCall.execute(proposalId); + await testSetup.genericSchemeMultiCall.execute(proposalId); assert(false, "Proposal call failed"); } catch(error) { helpers.assertVMException(error); @@ -164,7 +182,7 @@ contract('GenericSchemeMultiCall', function(accounts) { var testSetup = await setup(accounts,[accounts[1]]); var callData = await createCallToActionMock(helpers.NULL_ADDRESS,actionMock); try { - await testSetup.GenericSchemeMultiCall.proposeCalls( + await testSetup.genericSchemeMultiCall.proposeCalls( [actionMock.address],[callData],[0],helpers.NULL_HASH); assert(false, "contractToCall is not whitelisted"); } catch(error) { @@ -176,7 +194,7 @@ contract('GenericSchemeMultiCall', function(accounts) { 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 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]}); }); @@ -185,10 +203,10 @@ contract('GenericSchemeMultiCall', function(accounts) { 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 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); + await testSetup.genericSchemeMultiCall.execute( proposalId); assert(false, "execute should fail if not executed from votingMachine"); } catch(error) { helpers.assertVMException(error); @@ -200,16 +218,16 @@ contract('GenericSchemeMultiCall', function(accounts) { 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 value = 50000; var callData = await createCallToActionMock(testSetup.org.avatar.address,actionMock); - var tx = await testSetup.GenericSchemeMultiCall.proposeCalls([actionMock.address],[callData],[value],helpers.NULL_HASH); + var tx = await testSetup.genericSchemeMultiCall.proposeCalls([actionMock.address,actionMock.address],[callData,callData],[value,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); await testSetup.genericSchemeParams.votingMachine.genesisProtocol.vote(proposalId,1,0,helpers.NULL_ADDRESS,{from:accounts[2]}); - tx = await testSetup.GenericSchemeMultiCall.execute(proposalId); - await testSetup.GenericSchemeMultiCall.getPastEvents('ProposalExecuted', { + tx = await testSetup.genericSchemeMultiCall.execute(proposalId); + await testSetup.genericSchemeMultiCall.getPastEvents('ProposalExecuted', { fromBlock: tx.blockNumber, toBlock: 'latest' }) @@ -217,19 +235,74 @@ contract('GenericSchemeMultiCall', function(accounts) { assert.equal(events[0].event,"ProposalExecuted"); assert.equal(events[0].args._proposalId,proposalId); }); - assert.equal(await web3.eth.getBalance(actionMock.address),value); + assert.equal(await web3.eth.getBalance(actionMock.address),value*2); + //try to execute another one within the same period should fail + tx = await testSetup.genericSchemeMultiCall.proposeCalls([actionMock.address,actionMock.address],[callData,callData],[value,value],helpers.NULL_HASH); + proposalId = await helpers.getValueFromLogs(tx, '_proposalId'); + await testSetup.genericSchemeParams.votingMachine.genesisProtocol.vote(proposalId,1,0,helpers.NULL_ADDRESS,{from:accounts[2]}); + try { + await testSetup.genericSchemeMultiCall.execute(proposalId); + assert(false, "cannot send more within the same period"); + } catch(error) { + helpers.assertVMException(error); + } + await helpers.increaseTime(100000); + tx = await testSetup.genericSchemeMultiCall.execute(proposalId); + await testSetup.genericSchemeMultiCall.getPastEvents('ProposalExecuted', { + fromBlock: tx.blockNumber, + toBlock: 'latest' + }) + .then(function(events){ + assert.equal(events[0].event,"ProposalExecuted"); + assert.equal(events[0].args._proposalId,proposalId); + }); + }); + + it("schemeconstrains eth value exceed limit", 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 = 100001; + 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); + await testSetup.genericSchemeParams.votingMachine.genesisProtocol.vote(proposalId,1,0,helpers.NULL_ADDRESS,{from:accounts[2]}); + try { + await testSetup.genericSchemeMultiCall.execute(proposalId); + assert(false, "cannot transfer eth amount"); + } catch(error) { + helpers.assertVMException(error); + } }); + it("schemeconstrains token value exceed limit", async function() { + var standardTokenMock = await ERC20Mock.new(accounts[0],1000); + var testSetup = await setup(accounts,[accounts[3]],0,true,standardTokenMock.address); + var encodedTokenApproval = await createCallToTokenApproval(standardTokenMock,accounts[3], 10001); + var tx = await testSetup.genericSchemeMultiCall.proposeCalls([standardTokenMock.address],[encodedTokenApproval],[0],helpers.NULL_HASH); + var proposalId = await helpers.getValueFromLogs(tx, '_proposalId'); + await testSetup.genericSchemeParams.votingMachine.genesisProtocol.vote(proposalId,1,0,helpers.NULL_ADDRESS,{from:accounts[2]}); + try { + await testSetup.genericSchemeMultiCall.execute(proposalId); + assert(false, "cannot approve token amount: periodSpendingTokensExceeded"); + } catch(error) { + helpers.assertVMException(error); + } + }); + 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 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', { + await testSetup.genericSchemeMultiCall.getPastEvents('ProposalExecutedByVotingMachine', { fromBlock: tx.blockNumber, toBlock: 'latest' }) @@ -247,14 +320,14 @@ contract('GenericSchemeMultiCall', function(accounts) { var callData1 = await createCallToActionMock(testSetup.org.avatar.address,actionMock); var callData2 = await createCallToActionMock(testSetup.org.avatar.address,actionMock); - var tx = await testSetup.GenericSchemeMultiCall.proposeCalls( + var tx = await testSetup.genericSchemeMultiCall.proposeCalls( [actionMock.address,actionMock2.address], [callData1,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', { + await testSetup.genericSchemeMultiCall.getPastEvents('ProposalExecutedByVotingMachine', { fromBlock: tx.blockNumber, toBlock: 'latest' }) @@ -271,64 +344,44 @@ contract('GenericSchemeMultiCall', function(accounts) { var testSetup = await setup(accounts,[actionMock.address,actionMock2.address],0,true,standardTokenMock.address); var callData1 = await createCallToActionMock(testSetup.org.avatar.address,actionMock); var callData2 = await createCallToActionMock(accounts[0],actionMock); - var tx = await testSetup.GenericSchemeMultiCall.proposeCalls( + var tx = await testSetup.genericSchemeMultiCall.proposeCalls( [actionMock.address,actionMock2.address], [callData1,callData2], [0,0], helpers.NULL_HASH); var proposalId = await helpers.getValueFromLogs(tx, '_proposalId'); - var proposal = await testSetup.GenericSchemeMultiCall.proposals(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]}); try { - await testSetup.GenericSchemeMultiCall.execute(proposalId); + await testSetup.genericSchemeMultiCall.execute(proposalId); assert(false, "Proposal call failed"); } catch(error) { helpers.assertVMException(error); } }); - it("execute proposeVote with multiple calls with votingMachine without whitelisted spender -negative 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 callData1 = await createCallToActionMock(testSetup.org.avatar.address,actionMock); - try { - await testSetup.GenericSchemeMultiCall.proposeCalls( - [actionMock.address,controllerAddr], - [callData1,encodedTokenApproval], - [0,0], - helpers.NULL_HASH); - assert(false, "spender contract not whitelisted"); - } catch(error) { - helpers.assertVMException(error); - } - }); 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,accounts[3]],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 encodedTokenApproval = await createCallToTokenApproval(standardTokenMock,accounts[3], 1000); var callData1 = await createCallToActionMock(testSetup.org.avatar.address,actionMock); - var tx = await testSetup.GenericSchemeMultiCall.proposeCalls( - [actionMock.address,controllerAddr], + var tx = await testSetup.genericSchemeMultiCall.proposeCalls( + [actionMock.address,standardTokenMock.address], [callData1,encodedTokenApproval], [0,0], helpers.NULL_HASH); var proposalId = await helpers.getValueFromLogs(tx, '_proposalId'); - var proposal = await testSetup.GenericSchemeMultiCall.proposals(proposalId); + var proposal = await testSetup.genericSchemeMultiCall.proposals(proposalId); assert.equal(proposal.exist,true); assert.equal(proposal.passed,false); + assert.equal(await standardTokenMock.allowance(testSetup.org.avatar.address,accounts[3]),0); await testSetup.genericSchemeParams.votingMachine.genesisProtocol.vote(proposalId,1,0,helpers.NULL_ADDRESS,{from:accounts[2]}); - await testSetup.GenericSchemeMultiCall.execute(proposalId); - await testSetup.GenericSchemeMultiCall.getPastEvents('ProposalCallExecuted', { + await testSetup.genericSchemeMultiCall.execute(proposalId); + await testSetup.genericSchemeMultiCall.getPastEvents('ProposalCallExecuted', { fromBlock: tx.blockNumber, toBlock: 'latest' }) @@ -338,56 +391,98 @@ contract('GenericSchemeMultiCall', function(accounts) { assert.equal(events[1].event,"ProposalCallExecuted"); assert.equal(events[1].args._proposalId,proposalId); }); + assert.equal(await standardTokenMock.allowance(testSetup.org.avatar.address,accounts[3]),1000); }); - it("cannot init without contract whitelist", async function() { + it("cannot init twice", async function() { var actionMock =await ActionMock.new(); var testSetup = await setup(accounts,[actionMock.address]); - var genericSchemeMultiCall =await GenericSchemeMultiCall.new(); - try { - await genericSchemeMultiCall.initialize( + await testSetup.genericSchemeMultiCall.initialize( testSetup.org.avatar.address, accounts[0], helpers.SOME_HASH, - [] + testSetup.schemeConstraints.address ); - assert(false, "contractWhitelist cannot be empty"); + assert(false, "cannot init twice"); } catch(error) { helpers.assertVMException(error); } }); - it("cannot init twice", async function() { + it("can init with multiple contracts on whitelist", async function() { + var dxDaoSchemeConstraints =await DxDaoSchemeConstraints.new(); + await dxDaoSchemeConstraints.initialize( + 1, + 0, + [], + [], + [accounts[0],accounts[1],accounts[2],accounts[3]] + ); + var contractsWhiteList = await dxDaoSchemeConstraints.getContractsWhiteList(); + assert.equal(contractsWhiteList[0],accounts[0]); + assert.equal(contractsWhiteList[1],accounts[1]); + assert.equal(contractsWhiteList[2],accounts[2]); + assert.equal(contractsWhiteList[3],accounts[3]); + + }); + + it("execute proposeVote with multiple calls with votingMachine without whitelisted spender", 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 encodedTokenApproval= await createCallToTokenApproval(standardTokenMock, accounts[3], 1000); + var callData1 = await createCallToActionMock(testSetup.org.avatar.address,actionMock); + try { + await testSetup.genericSchemeMultiCall.proposeCalls( + [actionMock.address], + [callData1,encodedTokenApproval], + [0,0], + helpers.NULL_HASH); + assert(false, "spender contract not whitelisted"); + } catch(error) { + helpers.assertVMException(error); + } + }); + + + it("none exist schemeConstraints for proposeCall", 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,false); + var encodedTokenApproval= await createCallToTokenApproval(standardTokenMock, accounts[3], 1000); + var callData1 = await createCallToActionMock(testSetup.org.avatar.address,actionMock); + await testSetup.genericSchemeMultiCall.proposeCalls( + [actionMock.address,actionMock.address], + [callData1,encodedTokenApproval], + [0,0], + helpers.NULL_HASH); + }); + + it("none exist schemeConstraints for executeCall", 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,false); + var value = 100001; + 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); + await testSetup.genericSchemeParams.votingMachine.genesisProtocol.vote(proposalId,1,0,helpers.NULL_ADDRESS,{from:accounts[2]}); + await testSetup.genericSchemeMultiCall.execute(proposalId); + }); + + it("execute none exist proposal", 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"); + await testSetup.genericSchemeMultiCall.execute(helpers.SOME_HASH); + assert(false, "cannot execute none exist proposal"); } 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(1),accounts[0]); - assert.equal(await genericSchemeMultiCall.whitelistedContracts(2),accounts[1]); - assert.equal(await genericSchemeMultiCall.whitelistedContracts(3),accounts[2]); - assert.equal(await genericSchemeMultiCall.whitelistedContracts(4),accounts[3]); - }); - });