Skip to content

feat(contracts): Add WalletScheme contract and tests #758

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
179 changes: 179 additions & 0 deletions contracts/schemes/WalletScheme.sol
Original file line number Diff line number Diff line change
@@ -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
);
}

}
267 changes: 267 additions & 0 deletions test/walletScheme.js
Original file line number Diff line number Diff line change
@@ -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);
}

});

});