diff --git a/contracts/utils/Redeemer.sol b/contracts/utils/Redeemer.sol index 00fd23d2..d50fd98e 100644 --- a/contracts/utils/Redeemer.sol +++ b/contracts/utils/Redeemer.sol @@ -2,6 +2,8 @@ pragma solidity 0.5.17; import "../universalSchemes/ContributionReward.sol"; import "../schemes/ContributionRewardExt.sol"; +import "../schemes/GenericSchemeMultiCall.sol"; +import "../schemes/GenericScheme.sol"; import "@daostack/infra/contracts/votingMachines/GenesisProtocol.sol"; import "openzeppelin-solidity/contracts/token/ERC20/IERC20.sol"; @@ -121,6 +123,98 @@ contract Redeemer { } } + /** + * @dev helper to redeem proposal and execute the multi-call + * It calls execute on the proposal if it is not yet executed. + * It tries to redeem reputation and stake from the GenesisProtocol. + * It tries to execute multi call to contracts from the GenericSchemeMultiCall + * This function does not emit events. + * A client should listen to GenesisProtocol and GenericSchemeMultiCall events + * to monitor redemption and execution operations. + * @param _genericSchemeMultiCall GenericSchemeMultiCall scheme + * @param _genesisProtocol genesisProtocol + * @param _proposalId the ID of the voting in the voting machine + * @param _beneficiary beneficiary + * @return gpRewards array + * gpRewards[0] - stakerTokenAmount + * gpRewards[1] - voterReputationAmount + * gpRewards[2] - proposerReputationAmount + * @return gpDaoBountyReward array + * gpDaoBountyReward[0] - staker dao bounty reward - + * will be zero for the case there is not enough tokens in avatar for the reward. + * gpDaoBountyReward[1] - staker potential dao bounty reward. + * @return executed bool true or false + * @return winningVote + * 1 - executed or closed and the winning vote is YES + * 2 - executed or closed and the winning vote is NO + */ + function redeemGenericSchemeMultiCall(GenericSchemeMultiCall _genericSchemeMultiCall, + GenesisProtocol _genesisProtocol, + bytes32 _proposalId, + address _beneficiary) + external + returns(uint[3] memory gpRewards, + uint[2] memory gpDaoBountyReward, + bool executed, + uint256 winningVote + ) + { + bool callGenericSchemeMultiCall; + (gpRewards, gpDaoBountyReward, executed, winningVote, callGenericSchemeMultiCall) = + genesisProtocolRedeem(_genesisProtocol, _proposalId, _beneficiary); + bool passedAndNotExecuted; + (,passedAndNotExecuted) = _genericSchemeMultiCall.proposals(_proposalId); + if (callGenericSchemeMultiCall && passedAndNotExecuted) { + _genericSchemeMultiCall.execute(_proposalId); + } + } + + /** + * @dev helper to redeem proposal and execute the call + * It calls execute on the proposal if it is not yet executed. + * It tries to redeem reputation and stake from the GenesisProtocol. + * It tries to execute call to contract from the GenericScheme + * This function does not emit events. + * A client should listen to GenesisProtocol and GenericScheme events + * to monitor redemption and execution operations. + * @param _genericScheme GenericScheme scheme + * @param _genesisProtocol genesisProtocol + * @param _proposalId the ID of the voting in the voting machine + * @param _beneficiary beneficiary + * @return gpRewards array + * gpRewards[0] - stakerTokenAmount + * gpRewards[1] - voterReputationAmount + * gpRewards[2] - proposerReputationAmount + * @return gpDaoBountyReward array + * gpDaoBountyReward[0] - staker dao bounty reward - + * will be zero for the case there is not enough tokens in avatar for the reward. + * gpDaoBountyReward[1] - staker potential dao bounty reward. + * @return executed bool true or false + * @return winningVote + * 1 - executed or closed and the winning vote is YES + * 2 - executed or closed and the winning vote is NO + */ + function redeemGenericScheme(GenericScheme _genericScheme, + GenesisProtocol _genesisProtocol, + bytes32 _proposalId, + address _beneficiary) + external + returns(uint[3] memory gpRewards, + uint[2] memory gpDaoBountyReward, + bool executed, + uint256 winningVote + ) + { + bool callGenericScheme; + (gpRewards, gpDaoBountyReward, executed, winningVote, callGenericScheme) = + genesisProtocolRedeem(_genesisProtocol, _proposalId, _beneficiary); + bool passedAndNotExecuted; + (,,,passedAndNotExecuted) = _genericScheme.organizationProposals(_proposalId); + if (callGenericScheme && passedAndNotExecuted) { + _genericScheme.execute(_proposalId); + } + } + function genesisProtocolRedeem(GenesisProtocol _genesisProtocol, bytes32 _proposalId, address _beneficiary) diff --git a/package-lock.json b/package-lock.json index 270f08fe..64a2f2d9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "@daostack/arc", - "version": "0.0.1-rc.51", + "version": "0.0.1-rc.52", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index fa96377c..be0ac970 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@daostack/arc", - "version": "0.0.1-rc.51", + "version": "0.0.1-rc.52", "description": "A platform for building DAOs", "files": [ "contracts/", diff --git a/test/genericscheme.js b/test/genericscheme.js index 408bb82a..53aa3e5e 100644 --- a/test/genericscheme.js +++ b/test/genericscheme.js @@ -7,6 +7,7 @@ const DAOTracker = artifacts.require("./DAOTracker.sol"); const ERC20Mock = artifacts.require("./ERC20Mock.sol"); const ActionMock = artifacts.require("./ActionMock.sol"); const Wallet = artifacts.require("./Wallet.sol"); +const Redeemer = artifacts.require("./Redeemer.sol"); class GenericSchemeParams { constructor() { @@ -162,6 +163,43 @@ contract('GenericScheme', function(accounts) { }); + it("execute proposeVote -positive decision - destination reverts and then active and executed with redeemer", 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 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.genericScheme.proposeCall(callData,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]}); + //actionMock revert because msg.sender is not the _addr param at actionMock thpugh the generic scheme not . + var organizationProposal = await testSetup.genericScheme.organizationProposals(proposalId); + assert.equal(organizationProposal.exist,true);//new contract address + assert.equal(organizationProposal.passed,true);//new contract address + //can call execute + await helpers.increaseTime(1001); + var redeemer = await Redeemer.new(); + var redeemRewards = await redeemer.redeemGenericScheme.call( + testSetup.genericScheme.address, + testSetup.genericSchemeParams.votingMachine.genesisProtocol.address, + proposalId, + accounts[0]); + assert.equal(redeemRewards[0][1],0); //redeemRewards[0] gpRewards + assert.equal(redeemRewards[0][2],60); + assert.equal(redeemRewards.executed,false); // GP already executed by vote + assert.equal(redeemRewards.winningVote,1); + tx = await redeemer.redeemGenericScheme( + testSetup.genericScheme.address, + testSetup.genericSchemeParams.votingMachine.genesisProtocol.address, + proposalId, + accounts[0]); + organizationProposal = await testSetup.genericScheme.organizationProposals(proposalId); + assert.equal(organizationProposal.exist,false);//new contract address + assert.equal(organizationProposal.passed,false);//new contract address + }); + it("execute proposeVote without return value-positive decision - check action", async function() { var actionMock =await ActionMock.new(); diff --git a/test/genericschememulticall.js b/test/genericschememulticall.js index 2e938af8..105d2d25 100644 --- a/test/genericschememulticall.js +++ b/test/genericschememulticall.js @@ -8,6 +8,7 @@ const ERC20Mock = artifacts.require("./ERC20Mock.sol"); const ActionMock = artifacts.require("./ActionMock.sol"); const DxDaoSchemeConstraints = artifacts.require("./DxDaoSchemeConstraints.sol"); const SimpleSchemeConstraints = artifacts.require("./SimpleSchemeConstraints.sol"); +const Redeemer = artifacts.require("./Redeemer.sol"); class GenericSchemeParams { constructor() { @@ -291,6 +292,76 @@ contract('GenericSchemeMultiCall', function(accounts) { }); }); + it("redeemer should fail if not executed from votingMachine", 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); + 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'); + var redeemer = await Redeemer.new(); + var redeemRewards = await redeemer.redeemGenericSchemeMultiCall.call( + testSetup.genericSchemeMultiCall.address, + testSetup.genericSchemeParams.votingMachine.genesisProtocol.address, + proposalId, + accounts[0]); + assert.equal(redeemRewards[0][1],0); //redeemRewards[0] gpRewards + assert.equal(redeemRewards[0][2],0); + assert.equal(redeemRewards.executed,false); + assert.equal(redeemRewards.winningVote,0); // Cannot redeem, so will not get the winning vote + tx = await redeemer.redeemGenericSchemeMultiCall( + testSetup.genericSchemeMultiCall.address, + testSetup.genericSchemeParams.votingMachine.genesisProtocol.address, + proposalId, + accounts[0]); + await testSetup.genericSchemeMultiCall.getPastEvents('ProposalExecuted', { + fromBlock: tx.blockNumber, + toBlock: 'latest' + }) + .then(function(events){ + assert.equal(events.length,0); + }); + }); + + it("execute proposeVote -positive decision - execute with redeemer", 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 = 50000; + var callData = await createCallToActionMock(testSetup.org.avatar.address,actionMock); + 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]}); + var redeemer = await Redeemer.new(); + var redeemRewards = await redeemer.redeemGenericSchemeMultiCall.call( + testSetup.genericSchemeMultiCall.address, + testSetup.genericSchemeParams.votingMachine.genesisProtocol.address, + proposalId, + accounts[0]); + assert.equal(redeemRewards[0][1],0); //redeemRewards[0] gpRewards + assert.equal(redeemRewards[0][2],60); + assert.equal(redeemRewards.executed,false); // GP already executed by vote + assert.equal(redeemRewards.winningVote,1); + tx = await redeemer.redeemGenericSchemeMultiCall( + testSetup.genericSchemeMultiCall.address, + testSetup.genericSchemeParams.votingMachine.genesisProtocol.address, + proposalId, + accounts[0]); + 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); + }); + assert.equal(await web3.eth.getBalance(actionMock.address),value*2); + }); + + it("schemeconstrains eth value exceed limit", async function() { var actionMock =await ActionMock.new(); var standardTokenMock = await ERC20Mock.new(accounts[0],1000);