diff --git a/.travis.yml b/.travis.yml index 6d7369c7..d109ba22 100644 --- a/.travis.yml +++ b/.travis.yml @@ -29,5 +29,5 @@ jobs: - stage: coverage name: "Solidity Test Coverage" - if: branch = arc-factory + if: branch = master-2 script: npm run coveralls diff --git a/contracts/schemes/TokenTrade.sol b/contracts/schemes/TokenTrade.sol new file mode 100644 index 00000000..7284b2a4 --- /dev/null +++ b/contracts/schemes/TokenTrade.sol @@ -0,0 +1,170 @@ +pragma solidity ^0.6.10; +// SPDX-License-Identifier: GPL-3.0 + +import "../votingMachines/VotingMachineCallbacks.sol"; + + +/** + * @title A scheme for trading ERC20 tokens with the DAO + */ +contract TokenTrade is VotingMachineCallbacks, ProposalExecuteInterface { + using SafeMath for uint256; + using SafeERC20 for IERC20; + + event TokenTradeProposed( + address indexed _avatar, + bytes32 indexed _proposalId, + string _descriptionHash, + address indexed _beneficiary, + IERC20 _sendToken, + uint256 _sendTokenAmount, + IERC20 _receiveToken, + uint256 _receiveTokenAmount + ); + + event TokenTradeProposalExecuted( + address indexed _avatar, + bytes32 indexed _proposalId, + address indexed _beneficiary, + IERC20 _sendToken, + uint256 _sendTokenAmount, + IERC20 _receiveToken, + uint256 _receiveTokenAmount + ); + + event ProposalExecuted(address indexed _avatar, bytes32 indexed _proposalId, int256 _decision); + + struct Proposal { + address beneficiary; + IERC20 sendToken; + uint256 sendTokenAmount; + IERC20 receiveToken; + uint256 receiveTokenAmount; + bool passed; + bool decided; + } + + mapping(bytes32=>Proposal) public proposals; + + /** + * @dev initialize + * @param _avatar the avatar this scheme referring to. + * @param _votingMachine the voting machines address to + * @param _votingParams genesisProtocol parameters - valid only if _voteParamsHash is zero + * @param _voteOnBehalf genesisProtocol parameter - valid only if _voteParamsHash is zero + * @param _voteParamsHash voting machine parameters. + */ + function initialize( + Avatar _avatar, + IntVoteInterface _votingMachine, + uint256[11] calldata _votingParams, + address _voteOnBehalf, + bytes32 _voteParamsHash + ) + external + { + super._initializeGovernance(_avatar, _votingMachine, _voteParamsHash, _votingParams, _voteOnBehalf); + } + + /** + * @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) + override + returns(bool) { + Proposal memory proposal = proposals[_proposalId]; + if (_decision == 1) { + proposals[_proposalId].passed = true; + } + proposals[_proposalId].decided = true; + + emit ProposalExecuted(address(avatar), _proposalId, _decision); + return true; + } + + + function execute(bytes32 _proposalId) public { + Proposal storage proposal = proposals[_proposalId]; + require(proposal.decided, "must be a decided proposal"); + if (proposal.passed) { + proposal.sendToken.safeTransfer(address(avatar), proposal.sendTokenAmount); + require( + Controller(avatar.owner()).externalTokenTransfer( + proposal.receiveToken, proposal.beneficiary, proposal.receiveTokenAmount + ), "failed to transfer tokens from the DAO" + ); + + emit TokenTradeProposalExecuted( + address(avatar), + _proposalId, + proposal.beneficiary, + proposal.sendToken, + proposal.sendTokenAmount, + proposal.receiveToken, + proposal.receiveTokenAmount + ); + delete proposals[_proposalId]; + } else { + Proposal memory _proposal = proposals[_proposalId]; + delete proposals[_proposalId]; + _proposal.sendToken.safeTransfer(address(_proposal.beneficiary), _proposal.sendTokenAmount); + } + } + + /** + * @dev propose to trade tokens with the DAO + * @param _sendToken token the proposer suggests to send to the DAO + * @param _sendTokenAmount token amount the proposer suggests to send to the DAO + * @param _receiveToken token the proposer asks to receive from the DAO + * @param _receiveTokenAmount token amount the proposer asks to receive from the DAO + * @param _descriptionHash proposal description hash + * @return proposalId an id which represents the proposal + */ + function proposeTokenTrade( + IERC20 _sendToken, + uint256 _sendTokenAmount, + IERC20 _receiveToken, + uint256 _receiveTokenAmount, + string memory _descriptionHash + ) + public + returns(bytes32 proposalId) + { + require( + address(_sendToken) != address(0) && address(_receiveToken) != address(0), + "Token address must not be null" + ); + require(_sendTokenAmount > 0 && _receiveTokenAmount > 0, "Token amount must be greater than 0"); + + _sendToken.safeTransferFrom(msg.sender, address(this), _sendTokenAmount); + proposalId = votingMachine.propose(2, voteParamsHash, msg.sender, address(avatar)); + + proposals[proposalId] = Proposal({ + beneficiary: msg.sender, + sendToken: _sendToken, + sendTokenAmount: _sendTokenAmount, + receiveToken: _receiveToken, + receiveTokenAmount: _receiveTokenAmount, + passed: false, + decided: false + }); + + proposalsBlockNumber[proposalId] = block.number; + + emit TokenTradeProposed( + address(avatar), + proposalId, + _descriptionHash, + msg.sender, + _sendToken, + _sendTokenAmount, + _receiveToken, + _receiveTokenAmount + ); + } +} diff --git a/contracts/utils/DAOFactory.sol b/contracts/utils/DAOFactory.sol index 8211739a..7af3ce22 100644 --- a/contracts/utils/DAOFactory.sol +++ b/contracts/utils/DAOFactory.sol @@ -5,7 +5,6 @@ pragma experimental ABIEncoderV2; import "../registry/App.sol"; import "../registry/ImplementationDirectory.sol"; -import "@daostack/upgrades/contracts/upgradeability/ProxyAdmin.sol"; import "@daostack/upgrades/contracts/upgradeability/AdminUpgradeabilityProxy.sol"; import "../libs/BytesLib.sol"; import "../controller/Controller.sol"; diff --git a/package-lock.json b/package-lock.json index b1cea8d5..8a84a6b3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "@daostack/arc-experimental", - "version": "0.1.2-rc.2", + "version": "0.1.2-rc.3", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 30fbedfa..853237dc 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@daostack/arc-experimental", - "version": "0.1.2-rc.2", + "version": "0.1.2-rc.3", "description": "A platform for building DAOs", "files": [ "contracts/", diff --git a/release-experimental.sh b/release-experimental.sh index b416ca30..5b5cc494 100644 --- a/release-experimental.sh +++ b/release-experimental.sh @@ -2,7 +2,7 @@ rm -rf ./node_modules rm -rf ./build -git checkout origin/arc-factory +git checkout origin/master-2 echo "npm install ..." npm i echo "truffle compile ..." diff --git a/test/helpers.js b/test/helpers.js index 8d8f75a0..09e8d7c6 100644 --- a/test/helpers.js +++ b/test/helpers.js @@ -38,6 +38,7 @@ const JoinAndQuit = artifacts.require("./JoinAndQuit.sol"); const FundingRequest = artifacts.require("./FundingRequest.sol"); const Dictator = artifacts.require("./Dictator.sol"); const ReputationAdmin = artifacts.require("./ReputationAdmin.sol"); +const TokenTrade = artifacts.require("./TokenTrade.sol"); const MAX_UINT_256 = '0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff'; @@ -159,7 +160,7 @@ const SOME_ADDRESS = '0x1000000000000000000000000000000000000000'; registration.rewarderMock = await RewarderMock.new(); registration.dictator = await Dictator.new(); registration.reputationAdmin = await ReputationAdmin.new(); - + registration.tokenTrade = await TokenTrade.new(); await implementationDirectory.setImplementation("DAOToken",registration.daoToken.address); await implementationDirectory.setImplementation("Reputation",registration.reputation.address); @@ -191,7 +192,7 @@ const SOME_ADDRESS = '0x1000000000000000000000000000000000000000'; await implementationDirectory.setImplementation("FundingRequest",registration.fundingRequest.address); await implementationDirectory.setImplementation("Dictator",registration.dictator.address); await implementationDirectory.setImplementation("ReputationAdmin",registration.reputationAdmin.address); - + await implementationDirectory.setImplementation("TokenTrade",registration.tokenTrade.address); registration.implementationDirectory = implementationDirectory; diff --git a/test/tokentrade.js b/test/tokentrade.js new file mode 100644 index 00000000..131e6060 --- /dev/null +++ b/test/tokentrade.js @@ -0,0 +1,405 @@ +const helpers = require("./helpers"); +const { NULL_ADDRESS } = require("./helpers"); +const TokenTrade = artifacts.require('./TokenTrade.sol'); +const ERC20Mock = artifacts.require("./ERC20Mock.sol"); + + +class TokenTradeParams { + constructor() { + } +} + +var registration; +const setupTokenTradeParams = async function( + accounts, + genesisProtocol, + token + ) { + var tokenTradeParams = new TokenTradeParams(); + + if (genesisProtocol === true) { + tokenTradeParams.votingMachine = await helpers.setupGenesisProtocol(accounts,token,helpers.NULL_ADDRESS); + tokenTradeParams.initdata = await new web3.eth.Contract(registration.tokenTrade.abi) + .methods + .initialize( + helpers.NULL_ADDRESS, + tokenTradeParams.votingMachine.genesisProtocol.address, + tokenTradeParams.votingMachine.uintArray, + tokenTradeParams.votingMachine.voteOnBehalf, + helpers.NULL_HASH + ).encodeABI(); + } else { + tokenTradeParams.votingMachine = await helpers.setupAbsoluteVote(helpers.NULL_ADDRESS,50); + tokenTradeParams.initdata = await new web3.eth.Contract(registration.tokenTrade.abi) + .methods + .initialize( + helpers.NULL_ADDRESS, + tokenTradeParams.votingMachine.absoluteVote.address, + [0,0,0,0,0,0,0,0,0,0,0], + helpers.NULL_ADDRESS, + tokenTradeParams.votingMachine.params + ).encodeABI(); + } + return tokenTradeParams; +}; + +const setup = async function (accounts,reputationAccount=0,genesisProtocol = false,tokenAddress=0) { + var testSetup = new helpers.TestSetup(); + registration = await helpers.registerImplementation(); + testSetup.reputationArray = [20,10,70]; + var account2; + if (reputationAccount === 0) { + account2 = accounts[2]; + } else { + account2 = reputationAccount; + } + testSetup.proxyAdmin = accounts[5]; + testSetup.tokenTradeParams= await setupTokenTradeParams( + accounts, + genesisProtocol, + tokenAddress + ); + + var permissions = "0x0000001f"; + [testSetup.org,tx] = await helpers.setupOrganizationWithArraysDAOFactory(testSetup.proxyAdmin, + accounts, + registration, + [accounts[0], + accounts[1], + account2], + [1000,0,0], + testSetup.reputationArray, + 0, + [web3.utils.fromAscii("TokenTrade")], + testSetup.tokenTradeParams.initdata, + [helpers.getBytesLength(testSetup.tokenTradeParams.initdata)], + [permissions], + "metaData" + ); + + testSetup.tokenTrade = await TokenTrade.at(await helpers.getSchemeAddress(registration.daoFactory.address,tx)); + testSetup.standardTokenMock = await ERC20Mock.new(accounts[0],10000); + return testSetup; +}; + +contract('TokenTrade', function(accounts) { + before(function() { + helpers.etherForEveryone(accounts); + }); + + it("proposeTokenTrade log", async function() { + var testSetup = await setup(accounts); + assert.equal(await testSetup.standardTokenMock.balanceOf(accounts[0]), 10000); + assert.equal(await testSetup.standardTokenMock.balanceOf(testSetup.tokenTrade.address), 0); + await testSetup.standardTokenMock.approve(testSetup.tokenTrade.address, 100); + + var tx = await testSetup.tokenTrade.proposeTokenTrade( + testSetup.standardTokenMock.address, + 100, + testSetup.standardTokenMock.address, + 101, + helpers.NULL_HASH + ); + assert.equal(tx.logs.length, 1); + assert.equal(tx.logs[0].event, "TokenTradeProposed"); + assert.equal(tx.logs[0].args._beneficiary, accounts[0]); + assert.equal(tx.logs[0].args._descriptionHash, helpers.NULL_HASH); + assert.equal(tx.logs[0].args._sendToken, testSetup.standardTokenMock.address); + assert.equal(tx.logs[0].args._sendTokenAmount, 100); + assert.equal(tx.logs[0].args._receiveToken, testSetup.standardTokenMock.address); + assert.equal(tx.logs[0].args._receiveTokenAmount, 101); + assert.equal(await testSetup.standardTokenMock.balanceOf(accounts[0]), 9900); + assert.equal(await testSetup.standardTokenMock.balanceOf(testSetup.tokenTrade.address), 100); + }); + + it("proposeTokenTrade should fail if tokens aren't transferred", async function() { + var testSetup = await setup(accounts); + + try { + await testSetup.tokenTrade.proposeTokenTrade( + testSetup.standardTokenMock.address, + 100, + testSetup.standardTokenMock.address, + 101, + helpers.NULL_HASH + ); + assert(false, "proposing should fail if token transfer fails"); + } catch(error) { + helpers.assertVMException(error); + } + }); + + it("proposeTokenTrade should fail if token not specified or amount is 0", async function() { + var testSetup = await setup(accounts); + await testSetup.standardTokenMock.approve(testSetup.tokenTrade.address, 100); + + try { + await testSetup.tokenTrade.proposeTokenTrade( + NULL_ADDRESS, + 100, + testSetup.standardTokenMock.address, + 101, + helpers.NULL_HASH + ); + assert(false, "proposing should fail if send token is null"); + } catch(error) { + helpers.assertVMException(error); + } + + try { + await testSetup.tokenTrade.proposeTokenTrade( + testSetup.standardTokenMock.address, + 100, + NULL_ADDRESS, + 101, + helpers.NULL_HASH + ); + assert(false, "proposing should fail if receive token is null"); + } catch(error) { + helpers.assertVMException(error); + } + + try { + await testSetup.tokenTrade.proposeTokenTrade( + testSetup.standardTokenMock.address, + 0, + testSetup.standardTokenMock.address, + 101, + helpers.NULL_HASH + ); + assert(false, "proposing should fail if send token amount is 0"); + } catch(error) { + helpers.assertVMException(error); + } + + try { + await testSetup.tokenTrade.proposeTokenTrade( + testSetup.standardTokenMock.address, + 100, + testSetup.standardTokenMock.address, + 0, + helpers.NULL_HASH + ); + assert(false, "proposing should fail if send token amount is 0"); + } catch(error) { + helpers.assertVMException(error); + } + }); + + it("execute proposal - fail - proposal should be deleted and funds returned", async function() { + var testSetup = await setup(accounts); + await testSetup.standardTokenMock.transfer(testSetup.org.avatar.address, 5000); + await testSetup.standardTokenMock.approve(testSetup.tokenTrade.address, 100); + assert.equal(await testSetup.standardTokenMock.balanceOf(accounts[0]), 5000); + assert.equal(await testSetup.standardTokenMock.balanceOf(testSetup.tokenTrade.address), 0); + + var tx = await testSetup.tokenTrade.proposeTokenTrade( + testSetup.standardTokenMock.address, + 100, + testSetup.standardTokenMock.address, + 101, + helpers.NULL_HASH + ); + var proposalId = await helpers.getValueFromLogs(tx, '_proposalId'); + + assert.equal(await testSetup.standardTokenMock.balanceOf(accounts[0]), 4900); + assert.equal(await testSetup.standardTokenMock.balanceOf(testSetup.tokenTrade.address), 100); + var proposal = await testSetup.tokenTrade.proposals(proposalId); + assert.equal(proposal.sendToken, testSetup.standardTokenMock.address); + + await testSetup.tokenTradeParams.votingMachine.absoluteVote.vote(proposalId,0,0,helpers.NULL_ADDRESS,{from:accounts[2]}); + tx = await testSetup.tokenTrade.execute(proposalId); + proposal = await testSetup.tokenTrade.proposals(proposalId); + assert.equal(proposal.sendToken, NULL_ADDRESS); + assert.equal(await testSetup.standardTokenMock.balanceOf(accounts[0]), 5000); + assert.equal(await testSetup.standardTokenMock.balanceOf(testSetup.tokenTrade.address), 0); + await testSetup.tokenTrade.getPastEvents("TokenTradeProposalExecuted", { + fromBlock: tx.blockNumber, + toBlock: 'latest' + }).then(function(events){ + assert.equal(events.length, 0); + }); + }); + + it("execute proposeVote - pass - proposal executed and deleted", async function() { + var testSetup = await setup(accounts); + await testSetup.standardTokenMock.transfer(testSetup.org.avatar.address, 5000); + await testSetup.standardTokenMock.approve(testSetup.tokenTrade.address, 100); + assert.equal(await testSetup.standardTokenMock.balanceOf(testSetup.org.avatar.address), 5000); + assert.equal(await testSetup.standardTokenMock.balanceOf(accounts[0]), 5000); + assert.equal(await testSetup.standardTokenMock.balanceOf(testSetup.tokenTrade.address), 0); + + var tx = await testSetup.tokenTrade.proposeTokenTrade( + testSetup.standardTokenMock.address, + 100, + testSetup.standardTokenMock.address, + 101, + helpers.NULL_HASH + ); + var proposalId = await helpers.getValueFromLogs(tx, '_proposalId'); + + assert.equal(await testSetup.standardTokenMock.balanceOf(testSetup.org.avatar.address), 5000); + assert.equal(await testSetup.standardTokenMock.balanceOf(accounts[0]), 4900); + assert.equal(await testSetup.standardTokenMock.balanceOf(testSetup.tokenTrade.address), 100); + var proposal = await testSetup.tokenTrade.proposals(proposalId); + assert.equal(proposal.sendToken, testSetup.standardTokenMock.address); + + await testSetup.tokenTradeParams.votingMachine.absoluteVote.vote(proposalId, 1, 0, helpers.NULL_ADDRESS, {from:accounts[2]}); + tx = await testSetup.tokenTrade.execute(proposalId); + proposal = await testSetup.tokenTrade.proposals(proposalId); + assert.equal(proposal.sendToken, NULL_ADDRESS); + assert.equal(await testSetup.standardTokenMock.balanceOf(testSetup.org.avatar.address), 4999); + assert.equal(await testSetup.standardTokenMock.balanceOf(accounts[0]), 5001); + assert.equal(await testSetup.standardTokenMock.balanceOf(testSetup.tokenTrade.address), 0); + await testSetup.tokenTrade.getPastEvents("TokenTradeProposalExecuted", { + fromBlock: tx.blockNumber, + toBlock: 'latest' + }).then(function(events){ + assert.equal(events[0].event, "TokenTradeProposalExecuted"); + assert.equal(events[0].args._avatar, testSetup.org.avatar.address); + assert.equal(events[0].args._proposalId, proposalId); + assert.equal(events[0].args._beneficiary, accounts[0]); + assert.equal(events[0].args._sendToken, testSetup.standardTokenMock.address); + assert.equal(events[0].args._sendTokenAmount, 100); + assert.equal(events[0].args._receiveToken, testSetup.standardTokenMock.address); + assert.equal(events[0].args._receiveTokenAmount, 101); + }); + }); + + it("execute proposal - pass - proposal should pass without execution if DAO doesn't have enough tokens", async function() { + var testSetup = await setup(accounts); + await testSetup.standardTokenMock.transfer(accounts[1], 5000); + await testSetup.standardTokenMock.approve(testSetup.tokenTrade.address, 100); + assert.equal(await testSetup.standardTokenMock.balanceOf(testSetup.org.avatar.address), 0); + assert.equal(await testSetup.standardTokenMock.balanceOf(accounts[0]), 5000); + assert.equal(await testSetup.standardTokenMock.balanceOf(testSetup.tokenTrade.address), 0); + + var tx = await testSetup.tokenTrade.proposeTokenTrade( + testSetup.standardTokenMock.address, + 100, + testSetup.standardTokenMock.address, + 101, + helpers.NULL_HASH + ); + var proposalId = await helpers.getValueFromLogs(tx, '_proposalId'); + + assert.equal(await testSetup.standardTokenMock.balanceOf(accounts[0]), 4900); + assert.equal(await testSetup.standardTokenMock.balanceOf(testSetup.tokenTrade.address), 100); + var proposal = await testSetup.tokenTrade.proposals(proposalId); + assert.equal(proposal.sendToken, testSetup.standardTokenMock.address); + + tx = await testSetup.tokenTradeParams.votingMachine.absoluteVote.vote(proposalId, 1, 0, helpers.NULL_ADDRESS, {from:accounts[2]}); + + proposal = await testSetup.tokenTrade.proposals(proposalId); + assert.equal(proposal.passed, true); + assert.equal(await testSetup.standardTokenMock.balanceOf(accounts[0]), 4900); + assert.equal(await testSetup.standardTokenMock.balanceOf(testSetup.tokenTrade.address), 100); + await testSetup.tokenTrade.getPastEvents("TokenTradeProposalExecuted", { + fromBlock: tx.blockNumber, + toBlock: 'latest' + }).then(function(events){ + assert.equal(events.length, 0); + }); + + await testSetup.standardTokenMock.transfer(testSetup.org.avatar.address, 5000, {from: accounts[1]}); + tx = await testSetup.tokenTrade.execute(proposalId); + + proposal = await testSetup.tokenTrade.proposals(proposalId); + assert.equal(proposal.sendToken, NULL_ADDRESS); + assert.equal(await testSetup.standardTokenMock.balanceOf(testSetup.org.avatar.address), 4999); + assert.equal(await testSetup.standardTokenMock.balanceOf(accounts[0]), 5001); + assert.equal(await testSetup.standardTokenMock.balanceOf(testSetup.tokenTrade.address), 0); + await testSetup.tokenTrade.getPastEvents("TokenTradeProposalExecuted", { + fromBlock: tx.blockNumber, + toBlock: 'latest' + }).then(function(events){ + assert.equal(events[0].event, "TokenTradeProposalExecuted"); + assert.equal(events[0].args._avatar, testSetup.org.avatar.address); + assert.equal(events[0].args._proposalId, proposalId); + assert.equal(events[0].args._beneficiary, accounts[0]); + assert.equal(events[0].args._sendToken, testSetup.standardTokenMock.address); + assert.equal(events[0].args._sendTokenAmount, 100); + assert.equal(events[0].args._receiveToken, testSetup.standardTokenMock.address); + assert.equal(events[0].args._receiveTokenAmount, 101); + }); + }); + + it("execute proposal - pass - proposal cannot execute before passed/ twice", async function() { + var testSetup = await setup(accounts); + await testSetup.standardTokenMock.transfer(testSetup.org.avatar.address, 5000); + await testSetup.standardTokenMock.approve(testSetup.tokenTrade.address, 100); + assert.equal(await testSetup.standardTokenMock.balanceOf(testSetup.org.avatar.address), 5000); + assert.equal(await testSetup.standardTokenMock.balanceOf(accounts[0]), 5000); + assert.equal(await testSetup.standardTokenMock.balanceOf(testSetup.tokenTrade.address), 0); + + var tx = await testSetup.tokenTrade.proposeTokenTrade( + testSetup.standardTokenMock.address, + 100, + testSetup.standardTokenMock.address, + 101, + helpers.NULL_HASH + ); + var proposalId = await helpers.getValueFromLogs(tx, '_proposalId'); + + assert.equal(await testSetup.standardTokenMock.balanceOf(testSetup.org.avatar.address), 5000); + assert.equal(await testSetup.standardTokenMock.balanceOf(accounts[0]), 4900); + assert.equal(await testSetup.standardTokenMock.balanceOf(testSetup.tokenTrade.address), 100); + var proposal = await testSetup.tokenTrade.proposals(proposalId); + assert.equal(proposal.sendToken, testSetup.standardTokenMock.address); + + try { + await testSetup.tokenTrade.execute(proposalId); + assert(false, "cannot execute before passed"); + } catch(error) { + helpers.assertVMException(error); + } + + await testSetup.tokenTradeParams.votingMachine.absoluteVote.vote(proposalId, 1, 0, helpers.NULL_ADDRESS, {from:accounts[2]}); + tx = await testSetup.tokenTrade.execute(proposalId); + + proposal = await testSetup.tokenTrade.proposals(proposalId); + assert.equal(proposal.sendToken, NULL_ADDRESS); + assert.equal(await testSetup.standardTokenMock.balanceOf(testSetup.org.avatar.address), 4999); + assert.equal(await testSetup.standardTokenMock.balanceOf(accounts[0]), 5001); + assert.equal(await testSetup.standardTokenMock.balanceOf(testSetup.tokenTrade.address), 0); + await testSetup.tokenTrade.getPastEvents("TokenTradeProposalExecuted", { + fromBlock: tx.blockNumber, + toBlock: 'latest' + }).then(function(events){ + assert.equal(events[0].event, "TokenTradeProposalExecuted"); + assert.equal(events[0].args._avatar, testSetup.org.avatar.address); + assert.equal(events[0].args._proposalId, proposalId); + assert.equal(events[0].args._beneficiary, accounts[0]); + assert.equal(events[0].args._sendToken, testSetup.standardTokenMock.address); + assert.equal(events[0].args._sendTokenAmount, 100); + assert.equal(events[0].args._receiveToken, testSetup.standardTokenMock.address); + assert.equal(events[0].args._receiveTokenAmount, 101); + }); + + try { + await testSetup.tokenTrade.execute(proposalId); + assert(false, "cannot execute twice"); + } catch(error) { + helpers.assertVMException(error); + } + }); + + it("cannot init twice", async function() { + var testSetup = await setup(accounts); + + try { + await testSetup.tokenTrade.initialize( + testSetup.org.avatar.address, + accounts[0], + [0,0,0,0,0,0,0,0,0,0,0], + helpers.NULL_ADDRESS, + helpers.SOME_HASH + ); + assert(false, "cannot init twice"); + } catch(error) { + helpers.assertVMException(error); + } + + }); + +});