diff --git a/contracts/schemes/JoinAndQuit.sol b/contracts/schemes/JoinAndQuit.sol index 62e65834..97d1be7f 100644 --- a/contracts/schemes/JoinAndQuit.sol +++ b/contracts/schemes/JoinAndQuit.sol @@ -37,6 +37,12 @@ contract JoinAndQuit is uint256 indexed _refund ); + event Refund( + address indexed _avatar, + address indexed _beneficiary, + uint256 indexed _refund + ); + event RedeemReputation( address indexed _avatar, bytes32 indexed _proposalId, @@ -216,35 +222,47 @@ contract JoinAndQuit is emit RedeemReputation(address(avatar), _proposalId, _proposal.proposedMember, reputation); } + /** + * @dev refund refund donator if the the funding goal did not reached till the funding goal deadline. + * @return refundAmount the refund amount + */ + function refund() public returns(uint256 refundAmount) { + // solhint-disable-next-line not-rely-on-time + require(now > fundingGoalDeadline, "can refund only after fundingGoalDeadline"); + require( + (avatar.db(FUNDED_BEFORE_DEADLINE_KEY).hashCompareWithLengthCheck(FUNDED_BEFORE_DEADLINE_VALUE) == false), + "can refund only if funding goal not reached"); + require(fundings[msg.sender].funding > 0, "no funds to refund"); + refundAmount = fundings[msg.sender].funding; + fundings[msg.sender].funding = 0; + sendToBeneficiary(refundAmount, msg.sender); + emit Refund(address(avatar), msg.sender, refundAmount); + } + /** * @dev rageQuit quit from the dao. * can be done on any time * REFUND = USER_DONATION * CURRENT_DAO_BALANCE / TOTAL_DONATIONS - * @return refund the refund amount + * @return refundAmount the refund amount */ - function rageQuit() public returns(uint256 refund) { + function rageQuit() public returns(uint256 refundAmount) { require(rageQuitEnable, "RageQuit disabled"); require(fundings[msg.sender].funding > 0, "no fund to RageQuit"); uint256 userDonation = fundings[msg.sender].funding; fundings[msg.sender].funding = 0; fundings[msg.sender].rageQuit = true; if (fundingToken == IERC20(0)) { - refund = userDonation.mul(address(avatar.vault()).balance).div(totalDonation); - require( - Controller( - avatar.owner()).sendEther(refund, msg.sender), "send ether failed"); + refundAmount = userDonation.mul(address(avatar.vault()).balance).div(totalDonation); } else { - refund = userDonation.mul(fundingToken.balanceOf(address(avatar))).div(totalDonation); - require( - Controller( - avatar.owner()).externalTokenTransfer(fundingToken, msg.sender, refund), "send token failed"); + refundAmount = userDonation.mul(fundingToken.balanceOf(address(avatar))).div(totalDonation); } + sendToBeneficiary(refundAmount, msg.sender); uint256 msgSenderReputation = avatar.nativeReputation().balanceOf(msg.sender); require( Controller( avatar.owner()).burnReputation(msgSenderReputation, msg.sender)); totalDonation = totalDonation.sub(userDonation); - emit RageQuit(address(avatar), msg.sender, refund); + emit RageQuit(address(avatar), msg.sender, refundAmount); } /** @@ -270,4 +288,21 @@ contract JoinAndQuit is } } + /** + * @dev sendToBeneficiary send amount of eth or token to beneficiary + * @param _amount the amount to send + * @param _beneficiary the beneficiary + */ + function sendToBeneficiary(uint256 _amount, address payable _beneficiary) private { + if (fundingToken == IERC20(0)) { + require( + Controller( + avatar.owner()).sendEther(_amount, _beneficiary), "send ether failed"); + } else { + require( + Controller( + avatar.owner()).externalTokenTransfer(fundingToken, _beneficiary, _amount), "send token failed"); + } + } + } diff --git a/test/joinandquit.js b/test/joinandquit.js index 952b7ce0..7ef6cd32 100644 --- a/test/joinandquit.js +++ b/test/joinandquit.js @@ -642,4 +642,90 @@ contract('JoinAndQuit', accounts => { } }); + it("refund", async function() { + var testSetup = await setup(accounts); + await testSetup.standardTokenMock.approve(testSetup.joinAndQuit.address,testSetup.minFeeToJoin,{from:accounts[3]}); + var donatorBalance = await testSetup.standardTokenMock.balanceOf(accounts[3]); + var tx = await testSetup.joinAndQuit.proposeToJoin( + "description-hash", + testSetup.minFeeToJoin, + {from:accounts[3]}); + + //Vote with reputation to trigger execution + var proposalId = await helpers.getValueFromLogs(tx, '_proposalId',1); + await testSetup.joinAndQuitParams.votingMachine.absoluteVote.vote(proposalId,1,0,helpers.NULL_ADDRESS,{from:accounts[2]}); + assert.equal(await testSetup.standardTokenMock.balanceOf(testSetup.org.avatar.address),testSetup.minFeeToJoin); + assert.equal((await testSetup.joinAndQuit.fundings(accounts[3])).funding,testSetup.minFeeToJoin); + try { + await testSetup.joinAndQuit.refund({from:accounts[3]}); + assert(false, 'cannot refund before deadline'); + } catch (ex) { + helpers.assertVMException(ex); + } + await helpers.increaseTime(testSetup.fundingGoalDeadline); + tx = await testSetup.joinAndQuit.refund({from:accounts[3]}); + assert.equal(tx.logs.length, 1); + assert.equal(tx.logs[0].event, "Refund"); + assert.equal(tx.logs[0].args._avatar, testSetup.org.avatar.address); + assert.equal(tx.logs[0].args._beneficiary, accounts[3]); + assert.equal(tx.logs[0].args._refund, testSetup.minFeeToJoin); + assert.equal((await testSetup.standardTokenMock.balanceOf(accounts[3])).toString(),donatorBalance.toString()); + }); + + it("refund - cannot if funding goal reached.", async function() { + var testSetup = await setup(accounts); + await testSetup.standardTokenMock.approve(testSetup.joinAndQuit.address,testSetup.fundingGoal+1,{from:accounts[3]}); + var tx = await testSetup.joinAndQuit.proposeToJoin( + "description-hash", + testSetup.fundingGoal+1, + {from:accounts[3]}); + + //Vote with reputation to trigger execution + var proposalId = await helpers.getValueFromLogs(tx, '_proposalId',1); + await testSetup.joinAndQuitParams.votingMachine.absoluteVote.vote(proposalId,1,0,helpers.NULL_ADDRESS,{from:accounts[2]}); + + await helpers.increaseTime(testSetup.fundingGoalDeadline); + try { + await testSetup.joinAndQuit.refund({from:accounts[3]}); + assert(false, 'cannot if funding goal reached'); + } catch (ex) { + helpers.assertVMException(ex); + } + }); + + it("refund with eth", async function() { + var testSetup = await setup(accounts,true); + var tx = await testSetup.joinAndQuit.proposeToJoin( + "description-hash", + testSetup.minFeeToJoin, + {from:accounts[3],value:testSetup.minFeeToJoin}); + + //Vote with reputation to trigger execution + var proposalId = await helpers.getValueFromLogs(tx, '_proposalId',1); + await testSetup.joinAndQuitParams.votingMachine.absoluteVote.vote(proposalId,1,0,helpers.NULL_ADDRESS,{from:accounts[2]}); + assert.equal((await testSetup.joinAndQuit.fundings(accounts[3])).funding,testSetup.minFeeToJoin); + try { + await testSetup.joinAndQuit.refund({from:accounts[3]}); + assert(false, 'cannot refund before deadline'); + } catch (ex) { + helpers.assertVMException(ex); + } + await helpers.increaseTime(testSetup.fundingGoalDeadline); + var balanceBefore = await avatarBalance(testSetup); + tx = await testSetup.joinAndQuit.refund({from:accounts[3]}); + assert.equal(tx.logs.length, 1); + assert.equal(tx.logs[0].event, "Refund"); + assert.equal(tx.logs[0].args._avatar, testSetup.org.avatar.address); + assert.equal(tx.logs[0].args._beneficiary, accounts[3]); + assert.equal(tx.logs[0].args._refund, testSetup.minFeeToJoin); + assert.equal(await avatarBalance(testSetup),balanceBefore - testSetup.minFeeToJoin); + try { + await testSetup.joinAndQuit.refund({from:accounts[3]}); + assert(false, 'cannot refund twice'); + } catch (ex) { + helpers.assertVMException(ex); + } + }); + + });