From db35acb2b2e9b6be057cb13919b5f562ba78f1b6 Mon Sep 17 00:00:00 2001
From: AugustoL <me@augustol.com>
Date: Mon, 1 Jun 2020 11:07:00 -0300
Subject: [PATCH] feat(contracts): Add WalletScheme contract and tests

Adds the wallet scheme contract, it is based in the genericScheme but it doesnt make the genericCall
to the controller, instead it can execte multiple staticCalls to any contract. (It does not allow
execute staticCalls on the scheme itself and controller).
---
 contracts/schemes/WalletScheme.sol | 179 +++++++++++++++++++
 test/walletScheme.js               | 267 +++++++++++++++++++++++++++++
 2 files changed, 446 insertions(+)
 create mode 100644 contracts/schemes/WalletScheme.sol
 create mode 100644 test/walletScheme.js

diff --git a/contracts/schemes/WalletScheme.sol b/contracts/schemes/WalletScheme.sol
new file mode 100644
index 00000000..2fd7532c
--- /dev/null
+++ b/contracts/schemes/WalletScheme.sol
@@ -0,0 +1,179 @@
+pragma solidity 0.5.17;
+pragma experimental ABIEncoderV2;
+
+import "@daostack/infra/contracts/votingMachines/IntVoteInterface.sol";
+import "@daostack/infra/contracts/votingMachines/VotingMachineCallbacksInterface.sol";
+import "../votingMachines/VotingMachineCallbacks.sol";
+
+
+/**
+ * @title WalletScheme.
+ * @dev  A scheme for proposing and executing calls to any contract except itself and controller
+ */
+contract WalletScheme is VotingMachineCallbacks, ProposalExecuteInterface {
+    event NewCallProposal(
+        address[] _to,
+        bytes32 indexed _proposalId,
+        bytes[]   _callData,
+        uint256[] _value,
+        string  _descriptionHash
+    );
+
+    event ProposalExecuted(
+        bytes32 indexed _proposalId,
+        bytes[] _genericCallReturnValue
+    );
+
+    event ProposalExecutedByVotingMachine(
+        bytes32 indexed _proposalId,
+        int256 _param
+    );
+
+    event ProposalDeleted(bytes32 indexed _proposalId);
+
+    // Details of a voting proposal:
+    struct CallProposal {
+        address[] to;
+        bytes[] callData;
+        uint256[] value;
+        bool exist;
+        bool passed;
+    }
+
+    mapping(bytes32=>CallProposal) public organizationProposals;
+
+    IntVoteInterface public votingMachine;
+    bytes32 public voteParams;
+    Avatar public avatar;
+    address public controller;
+
+    /**
+     * @dev initialize
+     * @param _avatar the avatar address
+     * @param _controller the controller address
+     * @param _votingMachine the voting machines address to
+     * @param _voteParams voting machine parameters.
+     */
+    function initialize(
+        Avatar _avatar,
+        address _controller,
+        IntVoteInterface _votingMachine,
+        bytes32 _voteParams
+    )
+    external
+    {
+        require(avatar == Avatar(0), "can be called only one time");
+        require(_avatar != Avatar(0), "avatar cannot be zero");
+        require(_controller != address(0), "controller cannot be zero");
+        avatar = _avatar;
+        controller = _controller;
+        votingMachine = _votingMachine;
+        voteParams = _voteParams;
+    }
+    
+    /**
+    * @dev Fallback function that allows the wallet to receive ETH
+    */
+    function() external payable {}
+
+    /**
+    * @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) {
+        CallProposal storage proposal = organizationProposals[_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 organizationProposals[_proposalId];
+            emit ProposalDeleted(_proposalId);
+        }
+
+        emit ProposalExecutedByVotingMachine(_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 {
+        CallProposal storage proposal = organizationProposals[_proposalId];
+        require(proposal.exist, "must be a live proposal");
+        require(proposal.passed, "proposal must passed by voting machine");
+        proposal.exist = false;
+        bytes[] memory genericCallReturnValues = new bytes[](proposal.to.length);
+        bytes memory genericCallReturnValue;
+        bool success;
+        for(uint i = 0; i < proposal.to.length; i ++) {
+          (success, genericCallReturnValue) =
+          address(proposal.to[i]).call.value(proposal.value[i])(proposal.callData[i]);
+          genericCallReturnValues[i] = genericCallReturnValue;
+        }
+        if (success) {
+            delete organizationProposals[_proposalId];
+            emit ProposalDeleted(_proposalId);
+            emit ProposalExecuted(_proposalId, genericCallReturnValues);
+        } else {
+            proposal.exist = true;
+        }
+    }
+
+    /**
+    * @dev propose to call an address
+    *      The function trigger NewCallProposal event
+    * @param _to - The addresses to call
+    * @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 _to, bytes[] memory _callData, uint256[] memory _value, string memory _descriptionHash)
+    public
+    returns(bytes32)
+    {
+        for(uint i = 0; i < _to.length; i ++) {
+          require(_to[i] != controller && _to[i] != address(this), 'invalid proposal caller');
+        }
+        require(_to.length == _callData.length, 'invalid callData length');
+        require(_to.length == _value.length, 'invalid _value length');
+        
+        bytes32 proposalId = votingMachine.propose(2, voteParams, msg.sender, _to[0]);
+
+        organizationProposals[proposalId] = CallProposal({
+            to: _to,
+            callData: _callData,
+            value: _value,
+            exist: true,
+            passed: false
+        });
+        proposalsInfo[address(votingMachine)][proposalId] = ProposalInfo({
+            blockNumber: block.number,
+            avatar: avatar
+        });
+        emit NewCallProposal(_to, proposalId, _callData, _value, _descriptionHash);
+        return proposalId;
+    }
+    
+    function getOrganizationProposal(bytes32 proposalId) public view 
+      returns (address[] memory to, bytes[] memory callData, uint256[] memory value, bool exist, bool passed)
+    {
+      return (
+        organizationProposals[proposalId].to,
+        organizationProposals[proposalId].callData,
+        organizationProposals[proposalId].value,
+        organizationProposals[proposalId].exist,
+        organizationProposals[proposalId].passed
+      );
+    }
+
+}
diff --git a/test/walletScheme.js b/test/walletScheme.js
new file mode 100644
index 00000000..a86ae565
--- /dev/null
+++ b/test/walletScheme.js
@@ -0,0 +1,267 @@
+import * as helpers from './helpers';
+const constants = require('./constants');
+const WalletScheme = artifacts.require('./WalletScheme.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");
+const Wallet = artifacts.require("./Wallet.sol");
+
+export class WalletSchemeParams {
+  constructor() {
+  }
+}
+
+const setupWalletSchemeParams = async function(
+                                            walletScheme,
+                                            accounts,
+                                            contractToCall,
+                                            genesisProtocol = false,
+                                            tokenAddress = 0,
+                                            avatar,
+                                            controller
+                                            ) {
+  var walletSchemeParams = new WalletSchemeParams();
+  if (genesisProtocol === true){
+      walletSchemeParams.votingMachine = await helpers.setupGenesisProtocol(accounts,tokenAddress,0,helpers.NULL_ADDRESS);
+      await walletScheme.initialize(
+            avatar.address,
+            controller.address,
+            walletSchemeParams.votingMachine.genesisProtocol.address,
+            walletSchemeParams.votingMachine.params,
+      );
+    }
+  else {
+      walletSchemeParams.votingMachine = await helpers.setupAbsoluteVote(helpers.NULL_ADDRESS,50,walletScheme.address);
+      await walletScheme.initialize(
+            avatar.address,
+            controller.address,
+            walletSchemeParams.votingMachine.absoluteVote.address,
+            walletSchemeParams.votingMachine.params,
+      );
+  }
+  return walletSchemeParams;
+};
+
+const setup = async function (accounts,contractToCall = 0,reputationAccount=0,genesisProtocol = false,tokenAddress=0) {
+   var testSetup = new helpers.TestSetup();
+   testSetup.standardTokenMock = await ERC20Mock.new(accounts[1],100);
+   testSetup.walletScheme = await WalletScheme.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.walletSchemeParams= await setupWalletSchemeParams(
+     testSetup.walletScheme,accounts,contractToCall,genesisProtocol,tokenAddress,testSetup.org.avatar,controllerCreator
+   );
+   var permissions = "0x00000010";
+
+   await testSetup.daoCreator.setSchemes(testSetup.org.avatar.address,
+                                        [testSetup.walletScheme.address],
+                                        [helpers.NULL_HASH],[permissions],"metaData");
+
+   return testSetup;
+};
+
+const createCallToActionMock = async function(_sender,_actionMock) {
+  return await new web3.eth.Contract(_actionMock.abi).methods.test2(_sender).encodeABI();
+};
+
+contract('WalletScheme', function(accounts) {
+
+    it("proposeCalls log", async function() {
+
+       var actionMock =await ActionMock.new();
+       var testSetup = await setup(accounts,actionMock.address);
+       var callData = await createCallToActionMock(testSetup.walletScheme.address,actionMock);
+
+       var tx = await testSetup.walletScheme.proposeCalls([actionMock.address],[callData],[0],helpers.NULL_HASH);
+       assert.equal(tx.logs.length, 1);
+       assert.equal(tx.logs[0].event, "NewCallProposal");
+    });
+
+    it("execute proposeCalls -no decision - proposal data delete", async function() {
+       var actionMock =await ActionMock.new();
+       var testSetup = await setup(accounts,actionMock.address);
+       var callData = await createCallToActionMock(testSetup.walletScheme.address,actionMock);
+       var tx = await testSetup.walletScheme.proposeCalls([actionMock.address],[callData],[0],helpers.NULL_HASH);
+       var proposalId = await helpers.getValueFromLogs(tx, '_proposalId');
+       await testSetup.walletSchemeParams.votingMachine.absoluteVote.vote(proposalId,0,0,helpers.NULL_ADDRESS,{from:accounts[2]});
+       //check organizationsProposals after execution
+       var organizationProposal = await testSetup.walletScheme.getOrganizationProposal(proposalId);
+       assert.equal(organizationProposal.passed,false);
+       assert.equal(organizationProposal.callData[0],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.walletScheme.address,actionMock);
+        var tx = await testSetup.walletScheme.proposeCalls([actionMock.address],[callData],[0],helpers.NULL_HASH);
+        var proposalId = await helpers.getValueFromLogs(tx, '_proposalId');
+        var organizationProposal = await testSetup.walletScheme.getOrganizationProposal(proposalId);
+        assert.equal(organizationProposal[1][0],callData,helpers.NULL_HASH);
+        await testSetup.walletSchemeParams.votingMachine.absoluteVote.vote(proposalId,1,0,helpers.NULL_ADDRESS,{from:accounts[2]});
+        //check organizationsProposals after execution
+        organizationProposal = await testSetup.walletScheme.getOrganizationProposal(proposalId);
+        assert.equal(organizationProposal.callData[0],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.walletScheme.proposeCalls([actionMock.address],[callData],[0],helpers.NULL_HASH);
+       var proposalId = await helpers.getValueFromLogs(tx, '_proposalId');
+
+       await testSetup.walletSchemeParams.votingMachine.absoluteVote.vote(proposalId,1,0,helpers.NULL_ADDRESS,{from:accounts[2]});
+       //actionMock revert because msg.sender is not the _addr param at actionMock thpugh the generic scheme not .
+       var organizationProposal = await testSetup.walletScheme.getOrganizationProposal(proposalId);
+       assert.equal(organizationProposal.exist,true);//new contract address
+       assert.equal(organizationProposal.passed,true);//new contract address
+       //can call execute
+       await testSetup.walletScheme.execute( proposalId);
+    });
+
+
+    it("execute proposeVote -positive decision - destination reverts and then active", async function() {
+       var actionMock =await ActionMock.new();
+       var testSetup = await setup(accounts,actionMock.address);
+       var activationTime = (await web3.eth.getBlock("latest")).timestamp + 1000;
+       await actionMock.setActivationTime(activationTime);
+       var callData = await new web3.eth.Contract(actionMock.abi).methods.test3().encodeABI();
+       var tx = await testSetup.walletScheme.proposeCalls([actionMock.address],[callData],[0],helpers.NULL_HASH);
+       var proposalId = await helpers.getValueFromLogs(tx, '_proposalId');
+
+       await testSetup.walletSchemeParams.votingMachine.absoluteVote.vote(proposalId,1,0,helpers.NULL_ADDRESS,{from:accounts[2]});
+       //actionMock revert because msg.sender is not the _addr param at actionMock thpugh the generic scheme not .
+       var organizationProposal = await testSetup.walletScheme.getOrganizationProposal(proposalId);
+       assert.equal(organizationProposal.exist,true);//new contract address
+       assert.equal(organizationProposal.passed,true);//new contract address
+       //can call execute
+       await testSetup.walletScheme.execute( proposalId);
+       await helpers.increaseTime(1001);
+       await testSetup.walletScheme.execute( proposalId);
+
+       organizationProposal = await testSetup.walletScheme.getOrganizationProposal(proposalId);
+       assert.equal(organizationProposal.exist,false);//new contract address
+       assert.equal(organizationProposal.passed,false);//new contract address
+       try {
+         await testSetup.walletScheme.execute( proposalId);
+         assert(false, "cannot call execute after it been executed");
+       } 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.walletScheme.proposeCalls([actionMock.address],[encodeABI],[0],helpers.NULL_HASH);
+       var proposalId = await helpers.getValueFromLogs(tx, '_proposalId');
+
+       await testSetup.walletSchemeParams.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.walletScheme.proposeCalls([actionMock.address],[encodeABI],[0],helpers.NULL_HASH);
+       var proposalId = await helpers.getValueFromLogs(tx, '_proposalId');
+
+       try {
+         await testSetup.walletScheme.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.walletScheme.address,actionMock);
+       var tx = await testSetup.walletScheme.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.walletScheme.address, value: web3.utils.toWei('1', "ether")});
+       assert.equal(await web3.eth.getBalance(actionMock.address),0);
+       tx  = await testSetup.walletSchemeParams.votingMachine.genesisProtocol.vote(proposalId,1,0,helpers.NULL_ADDRESS,{from:accounts[2]});
+       await testSetup.walletScheme.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.walletScheme.address,actionMock);
+       var tx = await testSetup.walletScheme.proposeCalls([actionMock.address],[callData],[0],helpers.NULL_HASH);
+       var proposalId = await helpers.getValueFromLogs(tx, '_proposalId');
+       tx  = await testSetup.walletSchemeParams.votingMachine.genesisProtocol.vote(proposalId,2,0,helpers.NULL_ADDRESS,{from:accounts[2]});
+       await testSetup.walletScheme.getPastEvents('ProposalExecutedByVotingMachine', {
+             fromBlock: tx.blockNumber,
+             toBlock: 'latest'
+         })
+         .then(function(events){
+             assert.equal(events[0].event,"ProposalExecutedByVotingMachine");
+             assert.equal(events[0].args._param,2);
+        });
+      });
+
+      it("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);
+         var testSetup = await setup(accounts,wallet.address,0,true,standardTokenMock.address);
+         var callData = await new web3.eth.Contract(wallet.abi).methods.pay(accounts[1]).encodeABI();
+         var tx = await testSetup.walletScheme.proposeCalls([wallet.address],[callData],[0],helpers.NULL_HASH);
+         var proposalId = await helpers.getValueFromLogs(tx, '_proposalId');
+         assert.equal(await web3.eth.getBalance(wallet.address),web3.utils.toWei('1', "ether"));
+         await testSetup.walletSchemeParams.votingMachine.genesisProtocol.vote(proposalId,1,0,helpers.NULL_ADDRESS,{from:accounts[2]});
+         assert.equal(await web3.eth.getBalance(wallet.address),web3.utils.toWei('1', "ether"));
+         await wallet.transferOwnership(testSetup.walletScheme.address);
+         await testSetup.walletScheme.execute(proposalId);
+         assert.equal(await web3.eth.getBalance(wallet.address),0);
+      });
+
+      it("cannot init twice", async function() {
+         var actionMock =await ActionMock.new();
+         var testSetup = await setup(accounts,actionMock.address);
+
+         try {
+           await testSetup.walletScheme.initialize(
+             testSetup.org.avatar.address,
+             testSetup.daoCreator.address,
+             accounts[0],
+             accounts[0]
+           );
+           assert(false, "cannot init twice");
+         } catch(error) {
+           helpers.assertVMException(error);
+         }
+
+      });
+
+});