Skip to content

Commit 9c941a9

Browse files
committed
ragequit
1 parent 4a9dd8d commit 9c941a9

File tree

7 files changed

+112
-31
lines changed

7 files changed

+112
-31
lines changed

contracts/v3/adapters/Ragequit.sol

+2-4
Original file line numberDiff line numberDiff line change
@@ -24,13 +24,11 @@ contract RagequitContract is Module, AdapterGuard, ReentrancyGuard {
2424
}
2525

2626
function ragequit(Registry dao, uint256 sharesToBurn) public nonReentrant onlyMember(dao) {
27-
IMember memberContract = IMember(dao.getAddress(MEMBER_MODULE));
28-
2927
// FIXME: we still don't track the index to block the ragequit if member voted YES on a non-processed proposal
3028
// require(canRagequit(member.highestIndexYesVote), "cannot ragequit until highest index proposal member voted YES on is processed");
31-
require(memberContract.hasEnoughShares(dao, msg.sender, sharesToBurn), "insufficient shares");
29+
3230
IBank bank = IBank(dao.getAddress(BANK_MODULE));
33-
bank.burnShares(dao, msg.sender, sharesToBurn);
31+
bank.ragequit(dao, msg.sender, sharesToBurn);
3432

3533
emit Ragequit(msg.sender, sharesToBurn);
3634
}

contracts/v3/core/banking/Bank.sol

+8-4
Original file line numberDiff line numberDiff line change
@@ -56,16 +56,20 @@ contract BankContract is IBank, Module, ModuleGuard {
5656
emit Transfer(GUILD, applicant, token, amount);
5757
}
5858

59-
function burnShares(Registry dao, address memberAddr, uint256 sharesToBurn) override external onlyModule(dao) {
59+
function ragequit(Registry dao, address memberAddr, uint256 sharesToBurn) override external onlyModule(dao) {
60+
//Get the total shares before burning member shares
6061
IMember memberContract = IMember(dao.getAddress(MEMBER_MODULE));
6162
uint256 totalShares = memberContract.getTotalShares();
63+
//Burn shares if member has enough shares
64+
memberContract.burnShares(dao, memberAddr, sharesToBurn);
65+
//Update internal Guild and Member balances
6266
for (uint256 i = 0; i < states[address(dao)].tokens.length; i++) {
6367
address token = states[address(dao)].tokens[i];
6468
uint256 amountToRagequit = fairShare(states[address(dao)].tokenBalances[GUILD][token], sharesToBurn, totalShares);
6569
if (amountToRagequit > 0) { // gas optimization to allow a higher maximum token limit
66-
// deliberately not using safemath here to keep overflows from preventing the function execution (which would break ragekicks)
67-
// if a token overflows, it is because the supply was artificially inflated to oblivion, so we probably don't care about it anyways
68-
require(states[address(dao)].tokenBalances[GUILD][token] >= amountToRagequit, "insufficient balance");
70+
// deliberately not using safemath here to keep overflows from preventing the function execution
71+
// (which would break ragekicks) if a token overflows,
72+
// it is because the supply was artificially inflated to oblivion, so we probably don't care about it anyways
6973
states[address(dao)].tokenBalances[GUILD][token] -= amountToRagequit;
7074
states[address(dao)].tokenBalances[memberAddr][token] += amountToRagequit;
7175
//TODO: do we want to emit an event for each token transfer?

contracts/v3/core/interfaces/IBank.sol

+1-1
Original file line numberDiff line numberDiff line change
@@ -10,5 +10,5 @@ interface IBank {
1010
function balanceOf(Registry dao, address tokenAddress, address account) external returns (uint256);
1111
function isNotReservedAddress(address applicant) external returns (bool);
1212
function transferFromGuild(Registry dao, address applicant, address tokenAddress, uint256 amount) external;
13-
function burnShares(Registry dao, address member, uint256 sharesToBurn) external;
13+
function ragequit(Registry dao, address member, uint256 sharesToBurn) external;
1414
}

contracts/v3/core/interfaces/IMember.sol

+1-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ interface IMember {
88
function isActiveMember(Registry dao, address member) external returns (bool);
99
function memberAddress(Registry dao, address memberOrDelegateKey) external returns (address);
1010
function updateMember(Registry dao, address applicant, uint256 shares) external;
11+
function burnShares(Registry dao, address memberAddr, uint256 shares) external;
1112
function nbShares(Registry dao, address member) external view returns (uint256);
12-
function hasEnoughShares(Registry dao, address memberAddr, uint256 sharesToBurn) external view returns(bool);
1313
function getTotalShares() external view returns(uint256);
1414
}

contracts/v3/core/membership/Member.sol

+20-3
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,14 @@ import '../Registry.sol';
66
import '../Module.sol';
77
import '../interfaces/IMember.sol';
88
import '../interfaces/IBank.sol';
9+
import '../../utils/SafeMath.sol';
910
import '../../helpers/FlagHelper.sol';
1011
import '../../guards/ModuleGuard.sol';
1112
import '../../guards/ReentrancyGuard.sol';
1213

1314
contract MemberContract is IMember, Module, ModuleGuard, ReentrancyGuard {
1415
using FlagHelper for uint256;
16+
using SafeMath for uint256;
1517

1618
event UpdateMember(address dao, address member, uint256 shares);
1719

@@ -40,13 +42,19 @@ contract MemberContract is IMember, Module, ModuleGuard, ReentrancyGuard {
4042
member.flags = 1;
4143
member.nbShares = shares;
4244

43-
totalShares += shares;
45+
totalShares = totalShares.add(shares);
4446

4547
emit UpdateMember(address(dao), memberAddr, shares);
4648
}
4749

48-
function hasEnoughShares(Registry dao, address memberAddr, uint256 sharesToBurn) override external view onlyModule(dao) returns (bool) {
49-
return members[address(dao)][memberAddr].nbShares >= sharesToBurn;
50+
function burnShares(Registry dao, address memberAddr, uint256 sharesToBurn) override external onlyModule(dao) {
51+
require(_enoughSharesToBurn(dao, memberAddr, sharesToBurn), "insufficient shares");
52+
53+
Member storage member = members[address(dao)][memberAddr];
54+
member.nbShares = member.nbShares.sub(sharesToBurn);
55+
totalShares = totalShares.sub(sharesToBurn);
56+
57+
emit UpdateMember(address(dao), memberAddr, member.nbShares);
5058
}
5159

5260
/**
@@ -59,4 +67,13 @@ contract MemberContract is IMember, Module, ModuleGuard, ReentrancyGuard {
5967
function getTotalShares() override external view returns(uint256) {
6068
return totalShares;
6169
}
70+
71+
/**
72+
* Internal Utility Functions
73+
*/
74+
75+
function _enoughSharesToBurn(Registry dao, address memberAddr, uint256 sharesToBurn) internal view returns (bool) {
76+
return sharesToBurn > 0 && members[address(dao)][memberAddr].nbShares >= sharesToBurn;
77+
}
78+
6279
}

test/adapters/ragequit.test.js

+79-15
Original file line numberDiff line numberDiff line change
@@ -57,10 +57,10 @@ contract('MolochV3 - Ragequit Adapter', async accounts => {
5757
}
5858
})
5959

60-
it("should be possible to a member to ragequit", async () => {
60+
it("should not be possible to a member to ragequit when the member does not have enough shares", async () => {
6161
const myAccount = accounts[1];
6262
const newMember = accounts[2];
63-
63+
6464
let dao = await createDao({}, myAccount);
6565

6666
const bankAddress = await dao.getAddress(sha3("bank"));
@@ -82,7 +82,7 @@ contract('MolochV3 - Ragequit Adapter', async accounts => {
8282
await dao.sendTransaction({ from: newMember, value: sharePrice.mul(toBN(10)).add(remaining), gasPrice: toBN("0") });
8383
//Get the new proposal id
8484
pastEvents = await proposal.getPastEvents();
85-
let { proposalId } = pastEvents[0].returnValues;
85+
let { proposalId } = pastEvents[0].returnValues;
8686

8787
//Sponsor the new proposal, vote and process it
8888
await onboarding.sponsorProposal(dao.address, proposalId, [], { from: myAccount, gasPrice: toBN("0") });
@@ -91,10 +91,8 @@ contract('MolochV3 - Ragequit Adapter', async accounts => {
9191
await onboarding.processProposal(dao.address, proposalId, { from: myAccount, gasPrice: toBN("0") });
9292

9393
//Check Guild Bank Balance
94-
9594
let guildBalance = await bank.balanceOf(dao.address, GUILD, ETH_TOKEN);
96-
let expectedGuildBalance = toBN("1200000000000000000");
97-
assert.equal(guildBalance.toString(), expectedGuildBalance.toString());
95+
assert.equal(guildBalance.toString(), "1200000000000000000".toString());
9896

9997
//Check Member Shares
10098
let shares = await member.nbShares(dao.address, newMember);
@@ -103,22 +101,88 @@ contract('MolochV3 - Ragequit Adapter', async accounts => {
103101
//Ragequit
104102
let ragequitAddress = await dao.getAddress(sha3('ragequit'));
105103
let ragequitContract = await RagequitContract.at(ragequitAddress);
104+
try {
105+
//Trying to Ragequit with shares + 1 to burn
106+
await ragequitContract.ragequit(dao.address, toBN("10000000000000001"), { from: newMember, gasPrice: toBN("0") });
107+
} catch (error){
108+
assert.equal(error.reason, "insufficient shares");
109+
}
110+
111+
try {
112+
//Trying to Ragequit 0 shares to burn
113+
await ragequitContract.ragequit(dao.address, toBN("0"), { from: newMember, gasPrice: toBN("0") });
114+
} catch (error) {
115+
assert.equal(error.reason, "insufficient shares");
116+
}
117+
118+
})
119+
120+
it("should be possible to a member to ragequit when the member has not voted on any proposals yet", async () => {
121+
const myAccount = accounts[1];
122+
const newMember = accounts[2];
123+
124+
let dao = await createDao({}, myAccount);
125+
126+
const bankAddress = await dao.getAddress(sha3("bank"));
127+
const bank = await BankContract.at(bankAddress);
128+
129+
const proposalAddress = await dao.getAddress(sha3("proposal"));
130+
const proposal = await ProposalContract.at(proposalAddress);
131+
132+
//Add funds to the Guild Bank after sposoring a member to join the Guild
133+
const onboardingAddress = await dao.getAddress(sha3('onboarding'));
134+
const onboarding = await OnboardingContract.at(onboardingAddress);
135+
136+
const votingAddress = await dao.getAddress(sha3("voting"));
137+
const voting = await VotingContract.at(votingAddress);
138+
139+
const memberAddress = await dao.getAddress(sha3("member"));
140+
const memberContract = await MemberContract.at(memberAddress);
141+
142+
await dao.sendTransaction({ from: newMember, value: sharePrice.mul(toBN(100)), gasPrice: toBN("0") });
143+
//Get the new proposal id
144+
let pastEvents = await proposal.getPastEvents();
145+
let { proposalId } = pastEvents[0].returnValues;
146+
147+
//Sponsor the new proposal, vote and process it
148+
await onboarding.sponsorProposal(dao.address, proposalId, [], { from: myAccount, gasPrice: toBN("0") });
149+
await voting.submitVote(dao.address, proposalId, 1, { from: myAccount, gasPrice: toBN("0") });
150+
await advanceTime(10000);
151+
await onboarding.processProposal(dao.address, proposalId, { from: myAccount, gasPrice: toBN("0") });
152+
153+
//Check Guild Bank Balance
154+
let guildBalance = await bank.balanceOf(dao.address, GUILD, ETH_TOKEN);
155+
assert.equal(guildBalance.toString(), "12000000000000000000".toString());
156+
157+
//Check Member Shares
158+
let shares = await memberContract.nbShares(dao.address, newMember);
159+
assert.equal(shares.toString(), "100000000000000000");
160+
161+
//Ragequit - burn all member shares
162+
let ragequitAddress = await dao.getAddress(sha3('ragequit'));
163+
let ragequitContract = await RagequitContract.at(ragequitAddress);
106164
await ragequitContract.ragequit(dao.address, toBN(shares), { from: newMember, gasPrice: toBN("0") });
107165

108166
//Check Guild Bank Balance
109-
// guildBalance = await bank.balanceOf(GUILD, token);
110-
// assert.equal(guildBalance.toString(), "0");
167+
guildBalance = await bank.balanceOf(dao.address, GUILD, ETH_TOKEN);
168+
assert.equal(guildBalance.toString(), "240"); //must be close to 0
111169

112-
// //Check Member Shares
113-
// shares = await member.nbShares(dao.address, newMember);
114-
// assert.equal(shares.toString(), "0");
170+
//Check Member Shares
171+
let newShares = await memberContract.nbShares(dao.address, newMember);
172+
assert.equal(newShares.toString(), "0");
115173

116174
//Check Ragequit Event
117-
// pastEvents = await proposal.getPastEvents();
118-
// proposalId = pastEvents[0].returnValues.proposalId;
119-
// assert.equal(proposalId, 1);
175+
pastEvents = await ragequitContract.getPastEvents();
176+
let {member, burnedShares} = pastEvents[0].returnValues;
177+
assert.equal(member.toString(), newMember.toString());
178+
assert.equal(burnedShares.toString(), shares.toString());
179+
})
180+
181+
it("should be possible to a member to ragequit even if the member voted YES on a proposal", async () => {
182+
//TODO
183+
})
120184

121-
//Check Member Balance for each avaiable token
185+
it("should be possible to a member to ragequit even if the member voted NO on a proposal", async () => {
122186
//TODO
123187
})
124188
});

utils/DaoFactory.js

+1-3
Original file line numberDiff line numberDiff line change
@@ -42,9 +42,7 @@ async function createDao(overridenModules, senderAccount) {
4242
let daoFactory = await DaoFactory.new(member.address, proposal.address, voting.address, ragequit.address, managing.address, financing.address, onboarding.address, bank.address,
4343
{ from: senderAccount, gasPrice: web3.utils.toBN("0") });
4444
const txInfo = await daoFactory.newDao(sharePrice, numberOfShares, 1000, { from: senderAccount, gasPrice: web3.utils.toBN("0") });
45-
console.log('************');
46-
console.log(txInfo.receipt.gasUsed);
47-
console.log('************');
45+
// console.log("\t Gas Used: " + txInfo.receipt.gasUsed);
4846
let pastEvents = await daoFactory.getPastEvents();
4947
let daoAddress = pastEvents[0].returnValues.dao;
5048
let dao = await Registry.at(daoAddress);

0 commit comments

Comments
 (0)