From f0f67c5ca7001a2bbdc07617a4fad6a11db18b46 Mon Sep 17 00:00:00 2001 From: benk10 Date: Thu, 16 Jul 2020 10:44:08 +0300 Subject: [PATCH 1/7] Add tradetoken scheme --- contracts/schemes/TokenTrade.sol | 159 +++++++++++++++ contracts/utils/DAOFactory.sol | 1 - package-lock.json | 2 +- package.json | 2 +- test/helpers.js | 5 +- test/tokentrade.js | 323 +++++++++++++++++++++++++++++++ 6 files changed, 487 insertions(+), 5 deletions(-) create mode 100644 contracts/schemes/TokenTrade.sol create mode 100644 test/tokentrade.js diff --git a/contracts/schemes/TokenTrade.sol b/contracts/schemes/TokenTrade.sol new file mode 100644 index 00000000..8a1c59f5 --- /dev/null +++ b/contracts/schemes/TokenTrade.sol @@ -0,0 +1,159 @@ +pragma solidity ^0.6.10; +// SPDX-License-Identifier: GPL-3.0 + +import "../votingMachines/VotingMachineCallbacks.sol"; + + +/** + * @title A scheme for join in a dao. + * - A member can be proposed to join in by sending a min amount of fee. + * - A member can ask to quite (RageQuit) a dao on any time. + * - A member can donate to a dao. + */ +contract TokenTrade is VotingMachineCallbacks, ProposalExecuteInterface { + using SafeMath for uint; + 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; + } + + 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]; + delete proposals[_proposalId]; + if (_decision == 1 && proposal.receiveToken.balanceOf(address(avatar)) >= proposal.receiveTokenAmount) { + 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 + ); + } else { + proposal.sendToken.safeTransfer(address(proposal.beneficiary), proposal.sendTokenAmount); + } + + emit ProposalExecuted(address(avatar), _proposalId, _decision); + return true; + } + + /** + * @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 an id which represents the proposal + */ + function proposeTokenTrade( + IERC20 _sendToken, + uint256 _sendTokenAmount, + IERC20 _receiveToken, + uint256 _receiveTokenAmount, + string memory _descriptionHash + ) + public + returns(bytes32) + { + 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); + bytes32 proposalId = votingMachine.propose(2, voteParamsHash, msg.sender, address(avatar)); + + proposals[proposalId] = Proposal({ + beneficiary: msg.sender, + sendToken: _sendToken, + sendTokenAmount: _sendTokenAmount, + receiveToken: _receiveToken, + receiveTokenAmount: _receiveTokenAmount + }); + + proposalsBlockNumber[proposalId] = block.number; + + emit TokenTradeProposed( + address(avatar), + proposalId, + _descriptionHash, + msg.sender, + _sendToken, + _sendTokenAmount, + _receiveToken, + _receiveTokenAmount + ); + + return proposalId; + } +} 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/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..9fd76ba1 --- /dev/null +++ b/test/tokentrade.js @@ -0,0 +1,323 @@ +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); + + tx = await testSetup.tokenTradeParams.votingMachine.absoluteVote.vote(proposalId,0,0,helpers.NULL_ADDRESS,{from:accounts[2]}); + + 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); + + 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.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 still fail 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.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("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); + } + + }); + +}); From 9e2b503c1cf7d124a21d68b759166d6fbe7d8d51 Mon Sep 17 00:00:00 2001 From: benk10 Date: Mon, 20 Jul 2020 18:49:19 +0300 Subject: [PATCH 2/7] Delay execution trade token --- contracts/schemes/TokenTrade.sol | 35 ++++++++++--- test/tokentrade.js | 85 +++++++++++++++++++++++++++++++- 2 files changed, 110 insertions(+), 10 deletions(-) diff --git a/contracts/schemes/TokenTrade.sol b/contracts/schemes/TokenTrade.sol index 8a1c59f5..1c70799d 100644 --- a/contracts/schemes/TokenTrade.sol +++ b/contracts/schemes/TokenTrade.sol @@ -43,6 +43,8 @@ contract TokenTrade is VotingMachineCallbacks, ProposalExecuteInterface { uint256 sendTokenAmount; IERC20 receiveToken; uint256 receiveTokenAmount; + bool exist; + bool passed; } mapping(bytes32=>Proposal) public proposals; @@ -79,8 +81,25 @@ contract TokenTrade is VotingMachineCallbacks, ProposalExecuteInterface { override returns(bool) { Proposal memory proposal = proposals[_proposalId]; - delete proposals[_proposalId]; - if (_decision == 1 && proposal.receiveToken.balanceOf(address(avatar)) >= proposal.receiveTokenAmount) { + if (_decision == 1) { + proposals[_proposalId].passed = true; + execute(_proposalId); + } else { + delete proposals[_proposalId]; + proposal.sendToken.safeTransfer(address(proposal.beneficiary), proposal.sendTokenAmount); + } + + emit ProposalExecuted(address(avatar), _proposalId, _decision); + return true; + } + + + function execute(bytes32 _proposalId) public returns(bool) { + Proposal storage proposal = proposals[_proposalId]; + require(proposal.exist, "must be a live proposal"); + require(proposal.passed, "proposal must passed by voting machine"); + if (proposal.receiveToken.balanceOf(address(avatar)) >= proposal.receiveTokenAmount) { + proposal.exist = false; proposal.sendToken.safeTransfer(address(avatar), proposal.sendTokenAmount); require( Controller(avatar.owner()).externalTokenTransfer( @@ -97,12 +116,10 @@ contract TokenTrade is VotingMachineCallbacks, ProposalExecuteInterface { proposal.receiveToken, proposal.receiveTokenAmount ); - } else { - proposal.sendToken.safeTransfer(address(proposal.beneficiary), proposal.sendTokenAmount); + delete proposals[_proposalId]; + return true; } - - emit ProposalExecuted(address(avatar), _proposalId, _decision); - return true; + return false; } /** @@ -138,7 +155,9 @@ contract TokenTrade is VotingMachineCallbacks, ProposalExecuteInterface { sendToken: _sendToken, sendTokenAmount: _sendTokenAmount, receiveToken: _receiveToken, - receiveTokenAmount: _receiveTokenAmount + receiveTokenAmount: _receiveTokenAmount, + exist: true, + passed: false }); proposalsBlockNumber[proposalId] = block.number; diff --git a/test/tokentrade.js b/test/tokentrade.js index 9fd76ba1..6905fab3 100644 --- a/test/tokentrade.js +++ b/test/tokentrade.js @@ -266,7 +266,7 @@ contract('TokenTrade', function(accounts) { }); }); - it("execute proposal - pass - proposal should still fail if DAO doesn't have enough tokens", async function() { + 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); @@ -290,16 +290,97 @@ contract('TokenTrade', function(accounts) { 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); + } + + 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.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.length, 0); + 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() { From b78c3a80098a0830cc3b916aca8ac36cb59e3988 Mon Sep 17 00:00:00 2001 From: benk10 Date: Mon, 20 Jul 2020 18:50:35 +0300 Subject: [PATCH 3/7] uint256 --- contracts/schemes/TokenTrade.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/schemes/TokenTrade.sol b/contracts/schemes/TokenTrade.sol index 1c70799d..5cb5c967 100644 --- a/contracts/schemes/TokenTrade.sol +++ b/contracts/schemes/TokenTrade.sol @@ -11,7 +11,7 @@ import "../votingMachines/VotingMachineCallbacks.sol"; * - A member can donate to a dao. */ contract TokenTrade is VotingMachineCallbacks, ProposalExecuteInterface { - using SafeMath for uint; + using SafeMath for uint256; using SafeERC20 for IERC20; event TokenTradeProposed( From 1449fcf4212403df868c950078eedd2dae3e1de6 Mon Sep 17 00:00:00 2001 From: benk10 Date: Mon, 20 Jul 2020 19:11:18 +0300 Subject: [PATCH 4/7] Execute later --- contracts/schemes/TokenTrade.sol | 21 +++++++++++---------- test/tokentrade.js | 11 ++++++----- 2 files changed, 17 insertions(+), 15 deletions(-) diff --git a/contracts/schemes/TokenTrade.sol b/contracts/schemes/TokenTrade.sol index 5cb5c967..768f1c04 100644 --- a/contracts/schemes/TokenTrade.sol +++ b/contracts/schemes/TokenTrade.sol @@ -45,6 +45,7 @@ contract TokenTrade is VotingMachineCallbacks, ProposalExecuteInterface { uint256 receiveTokenAmount; bool exist; bool passed; + bool decided; } mapping(bytes32=>Proposal) public proposals; @@ -83,22 +84,19 @@ contract TokenTrade is VotingMachineCallbacks, ProposalExecuteInterface { Proposal memory proposal = proposals[_proposalId]; if (_decision == 1) { proposals[_proposalId].passed = true; - execute(_proposalId); - } else { - delete proposals[_proposalId]; - proposal.sendToken.safeTransfer(address(proposal.beneficiary), proposal.sendTokenAmount); } + proposals[_proposalId].decided = true; emit ProposalExecuted(address(avatar), _proposalId, _decision); return true; } - function execute(bytes32 _proposalId) public returns(bool) { + function execute(bytes32 _proposalId) public { Proposal storage proposal = proposals[_proposalId]; require(proposal.exist, "must be a live proposal"); - require(proposal.passed, "proposal must passed by voting machine"); - if (proposal.receiveToken.balanceOf(address(avatar)) >= proposal.receiveTokenAmount) { + require(proposal.decided, "must be a decided proposal"); + if (proposal.passed) { proposal.exist = false; proposal.sendToken.safeTransfer(address(avatar), proposal.sendTokenAmount); require( @@ -117,9 +115,11 @@ contract TokenTrade is VotingMachineCallbacks, ProposalExecuteInterface { proposal.receiveTokenAmount ); delete proposals[_proposalId]; - return true; + } else { + Proposal memory _proposal = proposals[_proposalId]; + delete proposals[_proposalId]; + _proposal.sendToken.safeTransfer(address(_proposal.beneficiary), _proposal.sendTokenAmount); } - return false; } /** @@ -157,7 +157,8 @@ contract TokenTrade is VotingMachineCallbacks, ProposalExecuteInterface { receiveToken: _receiveToken, receiveTokenAmount: _receiveTokenAmount, exist: true, - passed: false + passed: false, + decided: false }); proposalsBlockNumber[proposalId] = block.number; diff --git a/test/tokentrade.js b/test/tokentrade.js index 6905fab3..131e6060 100644 --- a/test/tokentrade.js +++ b/test/tokentrade.js @@ -207,8 +207,8 @@ contract('TokenTrade', function(accounts) { var proposal = await testSetup.tokenTrade.proposals(proposalId); assert.equal(proposal.sendToken, testSetup.standardTokenMock.address); - tx = await testSetup.tokenTradeParams.votingMachine.absoluteVote.vote(proposalId,0,0,helpers.NULL_ADDRESS,{from:accounts[2]}); - + 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); @@ -244,8 +244,8 @@ contract('TokenTrade', function(accounts) { 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]}); - + 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); @@ -354,7 +354,8 @@ contract('TokenTrade', function(accounts) { helpers.assertVMException(error); } - tx = await testSetup.tokenTradeParams.votingMachine.absoluteVote.vote(proposalId, 1, 0, helpers.NULL_ADDRESS, {from:accounts[2]}); + 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); From eaecfc8df027327779023a8967afd4d541edd3cc Mon Sep 17 00:00:00 2001 From: benk10 Date: Wed, 22 Jul 2020 14:31:33 +0300 Subject: [PATCH 5/7] Update TokenTrade.sol --- contracts/schemes/TokenTrade.sol | 18 +++++------------- 1 file changed, 5 insertions(+), 13 deletions(-) diff --git a/contracts/schemes/TokenTrade.sol b/contracts/schemes/TokenTrade.sol index 768f1c04..c7956d78 100644 --- a/contracts/schemes/TokenTrade.sol +++ b/contracts/schemes/TokenTrade.sol @@ -5,10 +5,7 @@ import "../votingMachines/VotingMachineCallbacks.sol"; /** - * @title A scheme for join in a dao. - * - A member can be proposed to join in by sending a min amount of fee. - * - A member can ask to quite (RageQuit) a dao on any time. - * - A member can donate to a dao. + * @title A scheme for trading ERC20 tokens with the DAO */ contract TokenTrade is VotingMachineCallbacks, ProposalExecuteInterface { using SafeMath for uint256; @@ -43,7 +40,6 @@ contract TokenTrade is VotingMachineCallbacks, ProposalExecuteInterface { uint256 sendTokenAmount; IERC20 receiveToken; uint256 receiveTokenAmount; - bool exist; bool passed; bool decided; } @@ -94,10 +90,9 @@ contract TokenTrade is VotingMachineCallbacks, ProposalExecuteInterface { function execute(bytes32 _proposalId) public { Proposal storage proposal = proposals[_proposalId]; - require(proposal.exist, "must be a live proposal"); + require(address(_sendToken) != address(0), "must be a live proposal"); require(proposal.decided, "must be a decided proposal"); if (proposal.passed) { - proposal.exist = false; proposal.sendToken.safeTransfer(address(avatar), proposal.sendTokenAmount); require( Controller(avatar.owner()).externalTokenTransfer( @@ -129,7 +124,7 @@ contract TokenTrade is VotingMachineCallbacks, ProposalExecuteInterface { * @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 an id which represents the proposal + * @return proposalId an id which represents the proposal */ function proposeTokenTrade( IERC20 _sendToken, @@ -139,7 +134,7 @@ contract TokenTrade is VotingMachineCallbacks, ProposalExecuteInterface { string memory _descriptionHash ) public - returns(bytes32) + returns(bytes32 proposalId) { require( address(_sendToken) != address(0) && address(_receiveToken) != address(0), @@ -148,7 +143,7 @@ contract TokenTrade is VotingMachineCallbacks, ProposalExecuteInterface { require(_sendTokenAmount > 0 && _receiveTokenAmount > 0, "Token amount must be greater than 0"); _sendToken.safeTransferFrom(msg.sender, address(this), _sendTokenAmount); - bytes32 proposalId = votingMachine.propose(2, voteParamsHash, msg.sender, address(avatar)); + proposalId = votingMachine.propose(2, voteParamsHash, msg.sender, address(avatar)); proposals[proposalId] = Proposal({ beneficiary: msg.sender, @@ -156,7 +151,6 @@ contract TokenTrade is VotingMachineCallbacks, ProposalExecuteInterface { sendTokenAmount: _sendTokenAmount, receiveToken: _receiveToken, receiveTokenAmount: _receiveTokenAmount, - exist: true, passed: false, decided: false }); @@ -173,7 +167,5 @@ contract TokenTrade is VotingMachineCallbacks, ProposalExecuteInterface { _receiveToken, _receiveTokenAmount ); - - return proposalId; } } From 276bf7d35b0772beb0824c5fddb2bb465900b656 Mon Sep 17 00:00:00 2001 From: benk10 Date: Wed, 22 Jul 2020 14:34:14 +0300 Subject: [PATCH 6/7] arc-factory -> master-2 --- .travis.yml | 2 +- release-experimental.sh | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) 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/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 ..." From e22225278afdd5cc34fa6956e3012bb545a053e5 Mon Sep 17 00:00:00 2001 From: benk10 Date: Wed, 22 Jul 2020 15:30:00 +0300 Subject: [PATCH 7/7] Update TokenTrade.sol --- contracts/schemes/TokenTrade.sol | 1 - 1 file changed, 1 deletion(-) diff --git a/contracts/schemes/TokenTrade.sol b/contracts/schemes/TokenTrade.sol index c7956d78..7284b2a4 100644 --- a/contracts/schemes/TokenTrade.sol +++ b/contracts/schemes/TokenTrade.sol @@ -90,7 +90,6 @@ contract TokenTrade is VotingMachineCallbacks, ProposalExecuteInterface { function execute(bytes32 _proposalId) public { Proposal storage proposal = proposals[_proposalId]; - require(address(_sendToken) != address(0), "must be a live proposal"); require(proposal.decided, "must be a decided proposal"); if (proposal.passed) { proposal.sendToken.safeTransfer(address(avatar), proposal.sendTokenAmount);