@nodeset/contracts
Version:
Protocol for accessing NodeSet's Constellation Ethereum staking network
758 lines (690 loc) • 65 kB
JavaScript
import { increaseTime, getCurrentTime } from '../_utils/evm'
import { printTitle } from '../_utils/formatting';
import { shouldRevert } from '../_utils/testing';
import { compressABI } from '../_utils/contract';
import { registerNode, setNodeTrusted } from '../_helpers/node';
import { mintDummyRPL } from '../token/scenario-rpl-mint-fixed';
import { burnFixedRPL } from '../token/scenario-rpl-burn-fixed';
import { allowDummyRPL } from '../token/scenario-rpl-allow-fixed';
import { setDaoNodeTrustedBootstrapMember, setDAONodeTrustedBootstrapSetting, setDaoNodeTrustedBootstrapModeDisabled, setDaoNodeTrustedBootstrapUpgrade, setDaoNodeTrustedMemberRequired } from './scenario-dao-node-trusted-bootstrap';
import { daoNodeTrustedExecute, getDAOMemberIsValid, daoNodeTrustedPropose, daoNodeTrustedVote, daoNodeTrustedCancel, daoNodeTrustedMemberJoin, daoNodeTrustedMemberLeave, daoNodeTrustedMemberChallengeMake, daoNodeTrustedMemberChallengeDecide } from './scenario-dao-node-trusted';
import { proposalStates, getDAOProposalState, getDAOProposalStartTime, getDAOProposalEndTime, getDAOProposalExpires } from './scenario-dao-proposal';
import { assertBN } from '../_helpers/bn';
import { RocketDAONodeTrusted, RocketDAONodeTrustedActions, RocketDAONodeTrustedSettingsMembers, RocketDAONodeTrustedSettingsProposals, RocketTokenRPL, RocketMinipoolManager, RocketDAONodeTrustedUpgrade, RocketStorage } from '../_utils/artifacts';
export default function() {
contract('RocketDAONodeTrusted', async (accounts) => {
// Accounts
const [
guardian,
userOne,
registeredNode1,
registeredNode2,
registeredNode3,
registeredNodeTrusted1,
registeredNodeTrusted2,
registeredNodeTrusted3,
] = accounts;
// Mints fixed supply RPL, burns that for new RPL and gives it to the account
let rplMint = async function(_account, _amount) {
// Load contracts
const rocketTokenRPL = await RocketTokenRPL.deployed();
// Convert
_amount = web3.utils.toWei(_amount.toString(), 'ether');
// Mint RPL fixed supply for the users to simulate current users having RPL
await mintDummyRPL(_account, _amount, { from: guardian });
// Mint a large amount of dummy RPL to guardian, who then burns it for real RPL which is sent to nodes for testing below
await allowDummyRPL(rocketTokenRPL.address, _amount, { from: _account });
// Burn existing fixed supply RPL for new RPL
await burnFixedRPL(_amount, { from: _account });
}
// Allow the given account to spend this users RPL
let rplAllowanceDAO = async function(_account, _amount) {
// Load contracts
const rocketTokenRPL = await RocketTokenRPL.deployed();
const rocketDAONodeTrustedActions = await RocketDAONodeTrustedActions.deployed();
// Convert
_amount = web3.utils.toWei(_amount.toString(), 'ether');
// Approve now
await rocketTokenRPL.approve(rocketDAONodeTrustedActions.address, _amount, { from: _account });
}
// Add a new DAO member via bootstrap mode
let bootstrapMemberAdd = async function(_account, _id, _url) {
// Use helper now
await setNodeTrusted(_account, _id, _url, guardian);
}
// Setup
let rocketMinipoolManagerNew;
let rocketDAONodeTrustedUpgradeNew;
before(async () => {
// Load contracts
// Get RocketStorage
const rocketStorage = await RocketStorage.deployed();
// Register nodes
await registerNode({from: registeredNode1});
await registerNode({from: registeredNode2});
await registerNode({from: registeredNode3});
await registerNode({from: registeredNodeTrusted1});
await registerNode({from: registeredNodeTrusted2});
await registerNode({from: registeredNodeTrusted3});
// Add members to the DAO now
await bootstrapMemberAdd(registeredNodeTrusted1, 'rocketpool_1', 'node@home.com');
await bootstrapMemberAdd(registeredNodeTrusted2, 'rocketpool_2', 'node@home.com');
// Deploy new contracts
rocketMinipoolManagerNew = await RocketMinipoolManager.new(rocketStorage.address, {from: guardian});
rocketDAONodeTrustedUpgradeNew = await RocketDAONodeTrustedUpgrade.new(rocketStorage.address, {from: guardian});
// Set a small proposal cooldown
await setDAONodeTrustedBootstrapSetting(RocketDAONodeTrustedSettingsProposals, 'proposal.cooldown', 10, { from: guardian });
// Set a small vote delay
await setDAONodeTrustedBootstrapSetting(RocketDAONodeTrustedSettingsProposals, 'proposal.vote.delay.blocks', 4, { from: guardian });
});
//
// Start Tests
//
it(printTitle('userOne', 'fails to be added as a trusted node dao member as they are not a registered node'), async () => {
// Set as trusted dao member via bootstrapping
await shouldRevert(setDaoNodeTrustedBootstrapMember('rocketpool', 'node@home.com', userOne, {
from: guardian
}), 'Non registered node added to trusted node DAO', 'Invalid node');
});
it(printTitle('userOne', 'fails to add a bootstrap trusted node DAO member as non guardian'), async () => {
// Set as trusted dao member via bootstrapping
await shouldRevert(setDaoNodeTrustedBootstrapMember('rocketpool', 'node@home.com', registeredNode1, {
from: userOne
}), 'Non guardian registered node to trusted node DAO', 'Account is not a temporary guardian');
});
it(printTitle('guardian', 'cannot add the same member twice'), async () => {
// Set as trusted dao member via bootstrapping
await shouldRevert(setDaoNodeTrustedBootstrapMember('rocketpool', 'node@home.com', registeredNodeTrusted2, {
from: guardian
}), 'Guardian the same DAO member twice', 'This node is already part of the trusted node DAO');
});
it(printTitle('guardian', 'updates quorum setting while bootstrap mode is enabled'), async () => {
// Set as trusted dao member via bootstrapping
await setDAONodeTrustedBootstrapSetting(RocketDAONodeTrustedSettingsMembers, 'members.quorum', web3.utils.toWei('0.55'), {
from: guardian
});
});
it(printTitle('guardian', 'updates RPL bond setting while bootstrap mode is enabled'), async () => {
// Set RPL Bond at 10K RPL
await setDAONodeTrustedBootstrapSetting(RocketDAONodeTrustedSettingsMembers, 'members.rplbond', web3.utils.toWei('10000'), {
from: guardian
});
});
it(printTitle('userOne', 'fails to update RPL bond setting while bootstrap mode is enabled as they are not the guardian'), async () => {
// Update setting
await shouldRevert(setDAONodeTrustedBootstrapSetting(RocketDAONodeTrustedSettingsMembers, 'members.rplbond', web3.utils.toWei('10000'), {
from: userOne
}), 'UserOne changed RPL bond setting', 'Account is not a temporary guardian');
});
it(printTitle('guardian', 'fails to update setting after bootstrap mode is disabled'), async () => {
// Disable bootstrap mode
await setDaoNodeTrustedBootstrapModeDisabled({
from: guardian
})
// Update setting
await shouldRevert(setDAONodeTrustedBootstrapSetting(RocketDAONodeTrustedSettingsProposals, 'members.quorum', web3.utils.toWei('0.55'), {
from: guardian
}), 'Guardian updated setting after bootstrap mode is disabled', 'Bootstrap mode not engaged');
});
it(printTitle('guardian', 'fails to set quorum setting as 0% while bootstrap mode is enabled'), async () => {
// Update setting
await shouldRevert(setDAONodeTrustedBootstrapSetting(RocketDAONodeTrustedSettingsMembers, 'members.quorum', web3.utils.toWei('0'), {
from: guardian
}), 'Guardian changed quorum setting to invalid value', 'Quorum setting must be > 0 & <= 90%');
});
it(printTitle('guardian', 'fails to set quorum setting above 90% while bootstrap mode is enabled'), async () => {
// Update setting
await shouldRevert(setDAONodeTrustedBootstrapSetting(RocketDAONodeTrustedSettingsMembers, 'members.quorum', web3.utils.toWei('0.91'), {
from: guardian
}), 'Guardian changed quorum setting to invalid value', 'Quorum setting must be > 0 & <= 90%');
});
it(printTitle('registeredNode1', 'verify trusted node quorum votes required is correct'), async () => {
// Load contracts
const rocketDAONodeTrusted = await RocketDAONodeTrusted.deployed();
const rocketDAONodeTrustedSettings = await RocketDAONodeTrustedSettingsMembers.deployed();
// How many trusted nodes do we have?
let trustedNodeCount = await rocketDAONodeTrusted.getMemberCount({
from: registeredNode1,
});
// Get the current quorum threshold
let quorumThreshold = await rocketDAONodeTrustedSettings.getQuorum();
// Calculate the expected vote threshold
let expectedVotes = (Number(web3.utils.fromWei(quorumThreshold)) * Number(trustedNodeCount)).toFixed(2);
// Calculate it now on the contracts
let quorumVotes = await rocketDAONodeTrusted.getMemberQuorumVotesRequired({
from: registeredNode1,
});
// Verify
assert.strictEqual(expectedVotes, Number(web3.utils.fromWei(quorumVotes)).toFixed(2), "Expected vote threshold does not match contracts");
});
// The big test
it(printTitle('registeredNodeTrusted1&2', 'create two proposals for two new members that are voted in, one then chooses to leave and is allowed too'), async () => {
// Get the DAO settings
let daoNodesettings = await RocketDAONodeTrustedSettingsMembers.deployed();
// How much RPL is required for a trusted node bond?
let rplBondAmount = web3.utils.fromWei(await daoNodesettings.getRPLBond());
// Disable bootstrap mode
await setDaoNodeTrustedBootstrapModeDisabled({ from: guardian });
// We only have 2 members now that bootstrap mode is disabled and proposals can only be made with 3, lets get a regular node to join via the emergency method
// We'll allow the DAO to transfer our RPL bond before joining
await rplMint(registeredNode3, rplBondAmount);
await rplAllowanceDAO(registeredNode3, rplBondAmount);
await setDaoNodeTrustedMemberRequired('rocketpool_emergency_node_op', 'node3@home.com', {
from: registeredNode3,
});
// New Member 1
// Encode the calldata for the proposal
let proposalCalldata1 = web3.eth.abi.encodeFunctionCall(
{name: 'proposalInvite', type: 'function', inputs: [{type: 'string', name: '_id'},{type: 'string', name: '_url'}, {type: 'address', name: '_nodeAddress'}]},
['SaaS_Provider1', 'test@sass.com', registeredNode1]
);
// Add the proposal
let proposalID_1 = await daoNodeTrustedPropose('hey guys, can we add this cool SaaS member please?', proposalCalldata1, {
from: registeredNodeTrusted1
});
// New Member 2
// Encode the calldata for the proposal
let proposalCalldata2 = web3.eth.abi.encodeFunctionCall(
{name: 'proposalInvite', type: 'function', inputs: [{type: 'string', name: '_id'},{type: 'string', name: '_url'}, {type: 'address', name: '_nodeAddress'}]},
['SaaS_Provider2', 'test2@sass.com', registeredNode2]
);
// Add the proposal
let proposalID_2 = await daoNodeTrustedPropose('hey guys, can we add this cool SaaS member please?', proposalCalldata2, {
from: registeredNodeTrusted2
});
// Current time
let timeCurrent = await getCurrentTime(web3);
// Now increase time until the proposal is 'active' and can be voted on
await increaseTime(web3, (await getDAOProposalStartTime(proposalID_1)-timeCurrent)+2);
// Now lets vote for the new members
await daoNodeTrustedVote(proposalID_1, true, { from: registeredNodeTrusted1 });
await daoNodeTrustedVote(proposalID_1, true, { from: registeredNodeTrusted2 });
await daoNodeTrustedVote(proposalID_2, true, { from: registeredNodeTrusted1 });
await daoNodeTrustedVote(proposalID_2, true, { from: registeredNodeTrusted2 });
// Current time
timeCurrent = await getCurrentTime(web3);
// Fast forward to voting periods finishing
await increaseTime(web3, (await getDAOProposalEndTime(proposalID_1)-timeCurrent)+2);
// Proposal should be successful, lets execute it
await daoNodeTrustedExecute(proposalID_1, { from: registeredNodeTrusted1 });
await daoNodeTrustedExecute(proposalID_2, { from: registeredNodeTrusted1 });
// Member has now been invited to join, so lets do that
// We'll allow the DAO to transfer our RPL bond before joining
await rplMint(registeredNode1, rplBondAmount);
await rplAllowanceDAO(registeredNode1, rplBondAmount);
await rplMint(registeredNode2, rplBondAmount);
await rplAllowanceDAO(registeredNode2, rplBondAmount);
// Join now
await daoNodeTrustedMemberJoin({from: registeredNode1});
await daoNodeTrustedMemberJoin({from: registeredNode2});
// Add a small wait between member join and proposal
await increaseTime(web3, 2);
// Now registeredNodeTrusted2 wants to leave
// Encode the calldata for the proposal
let proposalCalldata3 = web3.eth.abi.encodeFunctionCall(
{name: 'proposalLeave', type: 'function', inputs: [{type: 'address', name: '_nodeAddress'}]},
[registeredNodeTrusted2]
);
// Add the proposal
let proposalID_3 = await daoNodeTrustedPropose('hey guys, can I please leave the DAO?', proposalCalldata3, {
from: registeredNodeTrusted2
});
// Current time
timeCurrent = await getCurrentTime(web3);
// Now mine blocks until the proposal is 'active' and can be voted on
await increaseTime(web3, (await getDAOProposalStartTime(proposalID_3)-timeCurrent)+2);
// Now lets vote
await daoNodeTrustedVote(proposalID_3, true, { from: registeredNodeTrusted1 });
await daoNodeTrustedVote(proposalID_3, true, { from: registeredNodeTrusted2 });
await daoNodeTrustedVote(proposalID_3, false, { from: registeredNode1 });
await daoNodeTrustedVote(proposalID_3, true, { from: registeredNode2 });
// Current time
timeCurrent = await getCurrentTime(web3);
// Fast forward to this voting period finishing
await increaseTime(web3, (await getDAOProposalEndTime(proposalID_3)-timeCurrent)+2);
// Proposal should be successful, lets execute it
await daoNodeTrustedExecute(proposalID_3, { from: registeredNodeTrusted2 });
// Member can now leave and collect any RPL bond
await daoNodeTrustedMemberLeave(registeredNodeTrusted2, {from: registeredNodeTrusted2});
});
// Test various proposal states
it(printTitle('registeredNodeTrusted1', 'creates a proposal and verifies the proposal states as it passes and is executed'), async () => {
// Add our 3rd member
await bootstrapMemberAdd(registeredNode1, 'rocketpool', 'node@home.com');
await increaseTime(web3, 60);
// Now registeredNodeTrusted2 wants to leave
// Encode the calldata for the proposal
let proposalCalldata = web3.eth.abi.encodeFunctionCall(
{name: 'proposalInvite', type: 'function', inputs: [{type: 'string', name: '_id'},{type: 'string', name: '_url'}, {type: 'address', name: '_nodeAddress'}]},
['SaaS_Provider', 'test@sass.com', registeredNode2]
);
// Add the proposal
let proposalID = await daoNodeTrustedPropose('hey guys, can we add this cool SaaS member please?', proposalCalldata, {
from: registeredNodeTrusted1
});
// Verify the proposal is pending
assertBN.equal(await getDAOProposalState(proposalID), proposalStates.Pending, 'Proposal state is not Pending');
// Verify voting will not work while pending
await shouldRevert(daoNodeTrustedVote(proposalID, true, { from: registeredNode1 }), 'Member voted while proposal was pending', 'Voting is not active for this proposal');
// Current time
let timeCurrent = await getCurrentTime(web3);
// Now increase time until the proposal is 'active' and can be voted on
await increaseTime(web3, (await getDAOProposalStartTime(proposalID)-timeCurrent)+2);
// Now lets vote
await daoNodeTrustedVote(proposalID, true, { from: registeredNode1 });
await daoNodeTrustedVote(proposalID, true, { from: registeredNodeTrusted2 });
await shouldRevert(daoNodeTrustedVote(proposalID, false, { from: registeredNodeTrusted1 }), 'Member voted after proposal has passed', 'Proposal has passed, voting is complete and the proposal can now be executed');
// Verify the proposal is successful
assertBN.equal(await getDAOProposalState(proposalID), proposalStates.Succeeded, 'Proposal state is not succeeded');
// Proposal has passed, lets execute it now
await daoNodeTrustedExecute(proposalID, { from: registeredNode1 });
// Verify the proposal has executed
assertBN.equal(await getDAOProposalState(proposalID), proposalStates.Executed, 'Proposal state is not executed');
});
// Test various proposal states
it(printTitle('registeredNodeTrusted1', 'creates a proposal and verifies the proposal states as it fails after it expires'), async () => {
// Add our 3rd member
await bootstrapMemberAdd(registeredNode1, 'rocketpool', 'node@home.com');
await increaseTime(web3, 60);
// Now registeredNodeTrusted2 wants to leave
// Encode the calldata for the proposal
let proposalCalldata = web3.eth.abi.encodeFunctionCall(
{name: 'proposalInvite', type: 'function', inputs: [{type: 'string', name: '_id'},{type: 'string', name: '_url'}, {type: 'address', name: '_nodeAddress'}]},
['SaaS_Provider', 'test@sass.com', registeredNode2]
);
// Add the proposal
let proposalID = await daoNodeTrustedPropose('hey guys, can we add this cool SaaS member please?', proposalCalldata, {
from: registeredNodeTrusted1
});
// Verify the proposal is pending
assertBN.equal(await getDAOProposalState(proposalID), proposalStates.Pending, 'Proposal state is not Pending');
// Verify voting will not work while pending
await shouldRevert(daoNodeTrustedVote(proposalID, true, { from: registeredNode1 }), 'Member voted while proposal was pending', 'Voting is not active for this proposal');
// Current time
let timeCurrent = await getCurrentTime(web3);
// Now increase time until the proposal is 'active' and can be voted on
await increaseTime(web3, (await getDAOProposalStartTime(proposalID)-timeCurrent)+2);
// Now lets vote
await daoNodeTrustedVote(proposalID, true, { from: registeredNode1 });
await daoNodeTrustedVote(proposalID, false, { from: registeredNodeTrusted2 });
await daoNodeTrustedVote(proposalID, false, { from: registeredNodeTrusted1 });
// Fast forward to this voting period finishing
await increaseTime(web3, (await getDAOProposalEndTime(proposalID)-timeCurrent)+2);
// Verify the proposal is defeated
assertBN.equal(await getDAOProposalState(proposalID), proposalStates.Defeated, 'Proposal state is not defeated');
// Proposal has failed, can we execute it anyway?
await shouldRevert(daoNodeTrustedExecute(proposalID, { from: registeredNode1 }), 'Executed defeated proposal', 'Proposal has not succeeded, has expired or has already been executed');;
});
it(printTitle('registeredNodeTrusted1', 'creates a proposal for registeredNode1 to join as a new member but cancels it before it passes'), async () => {
// Add our 3rd member so proposals can pass
await bootstrapMemberAdd(registeredNodeTrusted3, 'rocketpool_3', 'node3@home.com');
await increaseTime(web3, 60);
// Encode the calldata for the proposal
let proposalCalldata = web3.eth.abi.encodeFunctionCall(
{name: 'proposalInvite', type: 'function', inputs: [{type: 'string', name: '_id'},{type: 'string', name: '_url'}, {type: 'address', name: '_nodeAddress'}]},
['SaaS_Provider', 'test@sass.com', registeredNode1]
);
// Add the proposal
let proposalID = await daoNodeTrustedPropose('hey guys, can we add this cool SaaS member please?', proposalCalldata, {
from: registeredNodeTrusted1
});
// Current time
let timeCurrent = await getCurrentTime(web3);
// Now increase time until the proposal is 'active' and can be voted on
await increaseTime(web3, (await getDAOProposalStartTime(proposalID)-timeCurrent)+2);
// Now lets vote
await daoNodeTrustedVote(proposalID, true, { from: registeredNodeTrusted1 });
// Cancel now before it passes
await daoNodeTrustedCancel(proposalID, {from: registeredNodeTrusted1});
});
it(printTitle('registeredNodeTrusted1', 'creates a proposal for registeredNode1 to join as a new member, then attempts to again for registeredNode2 before cooldown has passed and that fails'), async () => {
// Add our 3rd member so proposals can pass
await bootstrapMemberAdd(registeredNodeTrusted3, 'rocketpool_3', 'node3@home.com');
await increaseTime(web3, 60);
// Setup our proposal settings
let proposalCooldownTime = 60 * 60;
// Update now while in bootstrap mode
await setDAONodeTrustedBootstrapSetting(RocketDAONodeTrustedSettingsProposals, 'proposal.cooldown.time', proposalCooldownTime, { from: guardian });
// Encode the calldata for the proposal
let proposalCalldata = web3.eth.abi.encodeFunctionCall(
{name: 'proposalInvite', type: 'function', inputs: [{type: 'string', name: '_id'},{type: 'string', name: '_url'}, {type: 'address', name: '_nodeAddress'}]},
['SaaS_Provider', 'test@sass.com', registeredNode1]
);
// Add the proposal
await daoNodeTrustedPropose('hey guys, can we add this cool SaaS member please?', proposalCalldata, {
from: registeredNodeTrusted1
});
// Encode the calldata for the proposal
let proposalCalldata2 = web3.eth.abi.encodeFunctionCall(
{name: 'proposalInvite2', type: 'function', inputs: [{type: 'string', name: '_id'},{type: 'string', name: '_url'}, {type: 'address', name: '_nodeAddress'}]},
['SaaS_Provider2', 'test2@sass.com', registeredNode2]
);
// Add the proposal
await shouldRevert(daoNodeTrustedPropose('hey guys, can we add this other cool SaaS member please?', proposalCalldata2, {
from: registeredNodeTrusted1
}), 'Add proposal before cooldown period passed', 'Member has not waited long enough to make another proposal');
// Current block
let timeCurrent = await getCurrentTime(web3);
// Now wait until the cooldown period expires and proposal can be made again
await increaseTime(web3, timeCurrent + proposalCooldownTime + 2);
// Try again
await daoNodeTrustedPropose('hey guys, can we add this other cool SaaS member please?', proposalCalldata2, {
from: registeredNodeTrusted1
});
});
it(printTitle('registeredNodeTrusted1', 'creates a proposal for registeredNode1 to join as a new member, registeredNode2 tries to vote on it, but fails as they joined after it was created'), async () => {
// Add our 3rd member so proposals can pass
await bootstrapMemberAdd(registeredNodeTrusted3, 'rocketpool_3', 'node3@home.com');
await increaseTime(web3, 60);
// Encode the calldata for the proposal
let proposalCalldata = web3.eth.abi.encodeFunctionCall(
{name: 'proposalInvite', type: 'function', inputs: [{type: 'string', name: '_id'},{type: 'string', name: '_url'}, {type: 'address', name: '_nodeAddress'}]},
['SaaS_Provider', 'test@sass.com', registeredNode1]
);
// Add the proposal
let proposalID = await daoNodeTrustedPropose('hey guys, can we add this cool SaaS member please?', proposalCalldata, {
from: registeredNodeTrusted1
});
// Now add a new member after that proposal was created
await bootstrapMemberAdd(registeredNode2, 'rocketpool', 'node@home.com');
// Current block
let timeCurrent = await getCurrentTime(web3);
// Now wait until the cooldown period expires and proposal can be made again
await increaseTime(web3, (await getDAOProposalStartTime(proposalID)-timeCurrent)+2);
// registeredNodeTrusted1 votes
await daoNodeTrustedVote(proposalID, true, { from: registeredNodeTrusted1 });
// registeredNode2 vote fails
await shouldRevert(daoNodeTrustedVote(proposalID, true, {
from: registeredNode2
}), 'Voted on proposal created before they joined', 'Member cannot vote on proposal created before they became a member');
});
it(printTitle('registeredNodeTrusted1', 'creates a proposal to leave the DAO and receive their RPL bond refund, proposal is denied as it would be under the min members required for the DAO'), async () => {
// Add our 3rd member so proposals can pass
await bootstrapMemberAdd(registeredNodeTrusted3, 'rocketpool_3', 'node3@home.com');
await increaseTime(web3, 60);
// Encode the calldata for the proposal
let proposalCalldata = web3.eth.abi.encodeFunctionCall(
{name: 'proposalLeave', type: 'function', inputs: [{type: 'address', name: '_nodeAddress'}]},
[registeredNodeTrusted1]
);
// Add the proposal
let proposalID = await daoNodeTrustedPropose('hey guys, can I please leave the DAO?', proposalCalldata, {
from: registeredNodeTrusted1
});
// Current time
let timeCurrent = await getCurrentTime(web3);
// Now increase time until the proposal is 'active' and can be voted on
await increaseTime(web3, (await getDAOProposalStartTime(proposalID)-timeCurrent)+2);
// Now lets vote
await daoNodeTrustedVote(proposalID, true, { from: registeredNodeTrusted1 });
await daoNodeTrustedVote(proposalID, true, { from: registeredNodeTrusted2 });
// Fast forward to this voting period finishing
await increaseTime(web3, (await getDAOProposalEndTime(proposalID)-timeCurrent)+2);
// Proposal should be successful, lets execute it
await shouldRevert(daoNodeTrustedExecute(proposalID, { from: registeredNode2 }), 'Member proposal successful to leave DAO when they shouldnt be able too', 'Member count will fall below min required');
});
it(printTitle('registeredNodeTrusted1', 'creates a proposal to kick registeredNodeTrusted2 with a 50% fine, it is successful and registeredNodeTrusted2 is kicked and receives 50% of their bond'), async () => {
// Add our 3rd member so proposals can pass
await bootstrapMemberAdd(registeredNodeTrusted3, 'rocketpool_3', 'node3@home.com');
await increaseTime(web3, 60);
// Get the DAO settings
const daoNode = await RocketDAONodeTrusted.deployed();
const rocketTokenRPL = await RocketTokenRPL.deployed();
// Add our 3rd member
await bootstrapMemberAdd(registeredNode1, 'rocketpool', 'node@home.com');
await increaseTime(web3, 60);
// How much bond has registeredNodeTrusted2 paid?
let registeredNodeTrusted2BondAmount = await daoNode.getMemberRPLBondAmount.call(registeredNodeTrusted2);
// How much to fine? 33%
let registeredNodeTrusted2BondAmountFine = registeredNodeTrusted2BondAmount.div('3'.BN);
// Encode the calldata for the proposal
let proposalCalldata = web3.eth.abi.encodeFunctionCall(
{name: 'proposalKick', type: 'function', inputs: [{type: 'address', name: '_nodeAddress'}, {type: 'uint256', name: '_rplFine'}]},
[registeredNodeTrusted2, registeredNodeTrusted2BondAmountFine]
);
// Get the RPL total supply
let rplTotalSupply1 = await rocketTokenRPL.totalSupply.call()
// Add the proposal
let proposalID = await daoNodeTrustedPropose('hey guys, this member hasn\'t logged on for weeks, lets boot them with a 33% fine!', proposalCalldata, {
from: registeredNodeTrusted1
});
// Current time
let timeCurrent = await getCurrentTime(web3);
// Now increase time until the proposal is 'active' and can be voted on
await increaseTime(web3, (await getDAOProposalStartTime(proposalID)-timeCurrent)+2);
// Now lets vote
await daoNodeTrustedVote(proposalID, true, { from: registeredNode1 });
await daoNodeTrustedVote(proposalID, false, { from: registeredNodeTrusted2 }); // Don't kick me
await daoNodeTrustedVote(proposalID, true, { from: registeredNodeTrusted1 });
await daoNodeTrustedVote(proposalID, true, { from: registeredNodeTrusted3 });
// Proposal has passed, lets execute it now
await daoNodeTrustedExecute(proposalID, { from: registeredNode1 });
// Member should be kicked now, let's check their RPL balance has their 33% bond returned
let rplBalance = await rocketTokenRPL.balanceOf.call(registeredNodeTrusted2);
//console.log(web3.utils.fromWei(await rocketTokenRPL.balanceOf.call(registeredNodeTrusted2)));
assertBN.equal((registeredNodeTrusted2BondAmount.sub(registeredNodeTrusted2BondAmountFine)), rplBalance, "registeredNodeTrusted2 remaining RPL balance is incorrect");
assert.isFalse(await getDAOMemberIsValid(registeredNodeTrusted2), "registeredNodeTrusted2 is still a member of the DAO");
// The 33% fine should be burned
let rplTotalSupply2 = await rocketTokenRPL.totalSupply.call()
assertBN.equal(rplTotalSupply1.sub(rplTotalSupply2), registeredNodeTrusted2BondAmountFine, "RPL total supply did not decrease by fine amount");
});
it(printTitle('registeredNode2', 'is made a new member after a proposal is created, they fail to vote on that proposal'), async () => {
// Add our 3rd member so proposals can pass
await bootstrapMemberAdd(registeredNodeTrusted3, 'rocketpool_3', 'node3@home.com');
await increaseTime(web3, 60);
// Encode the calldata for the proposal
let proposalCalldata = web3.eth.abi.encodeFunctionCall(
{name: 'proposalLeave', type: 'function', inputs: [{type: 'address', name: '_nodeAddress'}]},
[registeredNodeTrusted1]
);
// Add the proposal
let proposalID = await daoNodeTrustedPropose('hey guys, can I please leave the DAO?', proposalCalldata, {
from: registeredNodeTrusted1
});
// Register new member now
await bootstrapMemberAdd(registeredNode2, 'rocketpool', 'node@home.com');
// Current time
let timeCurrent = await getCurrentTime(web3);
// Now increase time until the proposal is 'active' and can be voted on
await increaseTime(web3, (await getDAOProposalStartTime(proposalID)-timeCurrent)+2);
// Now lets vote
await daoNodeTrustedVote(proposalID, true, { from: registeredNodeTrusted1 });
// New member attempts to vote on proposal started before they joined, fails
await shouldRevert(daoNodeTrustedVote(proposalID, true, { from: registeredNode2 }), 'Member voted on proposal they shouldn\'t be able too', 'Member cannot vote on proposal created before they became a member');
});
it(printTitle('registeredNodeTrusted2', 'fails to execute a successful proposal after it expires'), async () => {
// Add our 3rd member so proposals can pass
await bootstrapMemberAdd(registeredNodeTrusted3, 'rocketpool_3', 'node3@home.com');
await increaseTime(web3, 60);
// Encode the calldata for the proposal
let proposalCalldata = web3.eth.abi.encodeFunctionCall(
{name: 'proposalLeave', type: 'function', inputs: [{type: 'address', name: '_nodeAddress'}]},
[registeredNodeTrusted1]
);
// Add the proposal
let proposalID = await daoNodeTrustedPropose('hey guys, can I please leave the DAO?', proposalCalldata, {
from: registeredNodeTrusted1
});
// Current time
let timeCurrent = await getCurrentTime(web3);
// Now increase time until the proposal is 'active' and can be voted on
await increaseTime(web3, (await getDAOProposalStartTime(proposalID)-timeCurrent)+2);
// Now lets vote
await daoNodeTrustedVote(proposalID, true, { from: registeredNodeTrusted1 });
await daoNodeTrustedVote(proposalID, true, { from: registeredNodeTrusted2 });
// Fast forward to this voting period finishing and executing period expiring
await increaseTime(web3, (await getDAOProposalExpires(proposalID)-timeCurrent)+2);
// Verify correct expired status
assertBN.equal(await getDAOProposalState(proposalID), proposalStates.Expired, 'Proposal state is not Expired');
// Execution should fail
await shouldRevert(daoNodeTrustedExecute(proposalID, { from: registeredNode2 }), 'Member execute proposal after it had expired', 'Proposal has not succeeded, has expired or has already been executed');
});
it(printTitle('registeredNodeTrusted2', 'checks to see if a proposal has expired after being successfully voted for, but not executed'), async () => {
// Add our 3rd member so proposals can pass
await bootstrapMemberAdd(registeredNodeTrusted3, 'rocketpool_3', 'node3@home.com');
await increaseTime(web3, 60);
// Encode the calldata for the proposal
let proposalCalldata = web3.eth.abi.encodeFunctionCall(
{name: 'proposalLeave', type: 'function', inputs: [{type: 'address', name: '_nodeAddress'}]},
[registeredNodeTrusted1]
);
// Add the proposal
let proposalID = await daoNodeTrustedPropose('hey guys, can I please leave the DAO?', proposalCalldata, {
from: registeredNodeTrusted1
});
// Current time
let timeCurrent = await getCurrentTime(web3);
// Now increase time until the proposal is 'active' and can be voted on
await increaseTime(web3, (await getDAOProposalStartTime(proposalID)-timeCurrent)+2);
// Now lets vote
await daoNodeTrustedVote(proposalID, true, { from: registeredNodeTrusted1 });
await daoNodeTrustedVote(proposalID, true, { from: registeredNodeTrusted2 });
// Fast forward to this voting period finishing and executing period expiring
await increaseTime(web3, (await getDAOProposalExpires(proposalID)-timeCurrent)+2);
// Execution should fail
await shouldRevert(daoNodeTrustedExecute(proposalID, { from: registeredNode2 }), 'Member execute proposal after it had expired', 'Proposal has not succeeded, has expired or has already been executed');
// Cancel should fail
await shouldRevert(daoNodeTrustedCancel(proposalID, { from: registeredNodeTrusted1 }), 'Member cancelled proposal after it had expired', 'Proposal can only be cancelled if pending or active');
});
it(printTitle('registeredNodeTrusted1', 'challenges another members node to respond and it does successfully in the window required'), async () => {
// Add a 3rd member
await bootstrapMemberAdd(registeredNode1, 'rocketpool_3', 'node2@home.com');
// Update our challenge settings
let challengeWindowTime = 60 * 60;
let challengeCooldownTime = 60 * 60;
// Update now while in bootstrap mode
await setDAONodeTrustedBootstrapSetting(RocketDAONodeTrustedSettingsMembers, 'members.challenge.window', challengeWindowTime, { from: guardian });
await setDAONodeTrustedBootstrapSetting(RocketDAONodeTrustedSettingsMembers, 'members.challenge.cooldown', challengeCooldownTime, { from: guardian });
// Attempt to challenge a non-member
await shouldRevert(daoNodeTrustedMemberChallengeMake(registeredNode2, { from: registeredNodeTrusted1 }), 'A non member was challenged', 'Invalid trusted node');
// Challenge the 3rd member
await daoNodeTrustedMemberChallengeMake(registeredNode1, { from: registeredNodeTrusted1 });
// Attempt to challenge again
await shouldRevert(daoNodeTrustedMemberChallengeMake(registeredNode1, { from: registeredNodeTrusted1 }), 'Member was challenged again', 'Member is already being challenged');
// Attempt to challenge another member before cooldown has passed
await shouldRevert(daoNodeTrustedMemberChallengeMake(registeredNodeTrusted2, { from: registeredNodeTrusted1 }), 'Member challenged another user before cooldown had passed', 'You must wait for the challenge cooldown to pass before issuing another challenge');
// Have 3rd member respond to the challenge successfully
await daoNodeTrustedMemberChallengeDecide(registeredNode1, true, { from: registeredNode1 });
// Wait until the original initiator's cooldown window has passed and they attempt another challenge
await increaseTime(web3, challengeCooldownTime + 2);
await daoNodeTrustedMemberChallengeMake(registeredNode1, { from: registeredNodeTrusted1 });
// Fast forward to past the challenge window with the challenged node responding
await increaseTime(web3, challengeWindowTime + 2);
// Have 3rd member respond to the challenge successfully again, but after the challenge window has expired and before another member decides it
await daoNodeTrustedMemberChallengeDecide(registeredNode1, true, { from: registeredNode1 });
});
it(printTitle('registeredNodeTrusted1', 'challenges another members node to respond, they do not in the window required and lose their membership + bond'), async () => {
// Add a 3rd member
await bootstrapMemberAdd(registeredNode1, 'rocketpool_3', 'node2@home.com');
// Update our challenge settings
let challengeWindowTime = 60 * 60;
let challengeCooldownTime = 60 * 60;
// Update now while in bootstrap mode
await setDAONodeTrustedBootstrapSetting(RocketDAONodeTrustedSettingsMembers, 'members.challenge.window', challengeWindowTime, { from: guardian });
await setDAONodeTrustedBootstrapSetting(RocketDAONodeTrustedSettingsMembers, 'members.challenge.cooldown', challengeCooldownTime, { from: guardian });
// Try to challenge yourself
await shouldRevert(daoNodeTrustedMemberChallengeMake(registeredNode1, { from: registeredNode1 }), 'Member challenged themselves', 'You cannot challenge yourself');
// Challenge the 3rd member
await daoNodeTrustedMemberChallengeMake(registeredNode1, { from: registeredNodeTrusted1 });
// Attempt to decide a challenge on a member that hasn't been challenged
await shouldRevert(daoNodeTrustedMemberChallengeDecide(registeredNodeTrusted2, true, { from: registeredNodeTrusted1 }), 'Member decided challenge on member without a challenge', 'Member hasn\'t been challenged or they have successfully responded to the challenge already');
// Have another member try to decide the result before the window passes, it shouldn't change and they should still be a member
await shouldRevert(daoNodeTrustedMemberChallengeDecide(registeredNode1, true, { from: registeredNodeTrusted2 }), 'Member decided challenge before refute window passed', 'Refute window has not yet passed');
// Fast forward to past the challenge window with the challenged node responding
await increaseTime(web3, challengeWindowTime + 2);
// Decide the challenge now after the node hasn't responded in the challenge window
await daoNodeTrustedMemberChallengeDecide(registeredNode1, false, { from: registeredNodeTrusted2 });
});
it(printTitle('registeredNode2', 'as a regular node challenges a DAO members node to respond by paying ETH, they do not respond in the window required and lose their membership + bond'), async () => {
// Get the DAO settings
let daoNodesettings = await RocketDAONodeTrustedSettingsMembers.deployed();
// How much ETH is required for a regular node to challenge a DAO member
let challengeCost = await daoNodesettings.getChallengeCost();
// Add a 3rd member
await bootstrapMemberAdd(registeredNode1, 'rocketpool_3', 'node2@home.com');
await increaseTime(web3, 60);
// Update our challenge settings
let challengeWindowTime = 60 * 60;
let challengeCooldownTime = 60 * 60;
// Update now while in bootstrap mode
await setDAONodeTrustedBootstrapSetting(RocketDAONodeTrustedSettingsMembers, 'members.challenge.window', challengeWindowTime, { from: guardian });
await setDAONodeTrustedBootstrapSetting(RocketDAONodeTrustedSettingsMembers, 'members.challenge.cooldown', challengeCooldownTime, { from: guardian });
// Attempt to challenge a non member
await shouldRevert(daoNodeTrustedMemberChallengeMake(userOne, {
from: registeredNode2
}), 'Challenged a non DAO member', 'Invalid trusted node');
// Attempt to challenge as a non member
await shouldRevert(daoNodeTrustedMemberChallengeMake(registeredNodeTrusted2, {
from: userOne
}), 'Challenged a non DAO member', 'Invalid node');
// Challenge the 3rd member as a regular node, should revert as we haven't paid to challenge
await shouldRevert(daoNodeTrustedMemberChallengeMake(registeredNode1, {
from: registeredNode2
}), 'Regular node challenged DAO member without paying challenge fee', 'Non DAO members must pay ETH to challenge a members node');
// Ok pay now to challenge
await daoNodeTrustedMemberChallengeMake(registeredNode1, {
value: challengeCost,
from: registeredNode2
});
// Fast forward to past the challenge window with the challenged node responding
await increaseTime(web3, challengeWindowTime + 2);
// Decide the challenge now after the node hasn't responded in the challenge window
await daoNodeTrustedMemberChallengeDecide(registeredNode1, false, { from: registeredNodeTrusted2 });
});
it(printTitle('registered2', 'joins the DAO automatically as a member due to the min number of members falling below the min required'), async () => {
// Attempt to join as a non node operator
await shouldRevert(setDaoNodeTrustedMemberRequired('rocketpool_emergency_node_op', 'node2@home.com', {
from: userOne
}), 'Regular node joined DAO without bond during low member mode', 'Invalid node');
// Attempt to join without setting allowance for the bond
await shouldRevert(setDaoNodeTrustedMemberRequired('rocketpool_emergency_node_op', 'node2@home.com', {
from: registeredNode2
}), 'Regular node joined DAO without bond during low member mode', 'Not enough allowance given to RocketDAONodeTrusted contract for transfer of RPL bond tokens');
// Get the DAO settings
let daoNodesettings = await RocketDAONodeTrustedSettingsMembers.deployed();
// How much RPL is required for a trusted node bond?
let rplBondAmount = web3.utils.fromWei(await daoNodesettings.getRPLBond());
// We'll allow the DAO to transfer our RPL bond before joining
await rplMint(registeredNode2, rplBondAmount);
await rplAllowanceDAO(registeredNode2, rplBondAmount);
// Should just be 2 nodes in the DAO now which means a 3rd can join to make up the min count
await setDaoNodeTrustedMemberRequired('rocketpool_emergency_node_op', 'node2@home.com', {
from: registeredNode2,
});
});
it(printTitle('registered2', 'attempt to auto join the DAO automatically and fails as the DAO has the min member count required'), async () => {
// Add a 3rd member
await bootstrapMemberAdd(registeredNode1, 'rocketpool_3', 'node2@home.com');
// Get the DAO settings
let daoNodesettings = await RocketDAONodeTrustedSettingsMembers.deployed();
// How much RPL is required for a trusted node bond?
let rplBondAmount = web3.utils.fromWei(await daoNodesettings.getRPLBond());
// We'll allow the DAO to transfer our RPL bond before joining
await rplMint(registeredNode2, rplBondAmount);
await rplAllowanceDAO(registeredNode2, rplBondAmount);
// Should just be 2 nodes in the DAO now which means a 3rd can join to make up the min count
await shouldRevert(setDaoNodeTrustedMemberRequired('rocketpool_emergency_node_op', 'node2@home.com', {
from: registeredNode2,
}), 'Regular node joined DAO when not in low member mode', 'Low member mode not engaged');
});
/*** Upgrade Contacts & ABI *************/
// Contracts
it(printTitle('guardian', 'can upgrade a contract in bootstrap mode'), async () => {
await setDaoNodeTrustedBootstrapUpgrade('upgradeContract', 'rocketNodeManager', rocketMinipoolManagerNew.abi, rocketMinipoolManagerNew.address, {
from: guardian,
});
});
it(printTitle('guardian', 'can upgrade the upgrade contract'), async () => {
await setDaoNodeTrustedBootstrapUpgrade('upgradeContract', 'rocketDAONodeTrustedUpgrade', rocketDAONodeTrustedUpgradeNew.abi, rocketDAONodeTrustedUpgradeNew.address, {
from: guardian,
});
});
it(printTitle('userOne', 'cannot upgrade a contract in bootstrap mode'), async () => {
await shouldRevert(setDaoNodeTrustedBootstrapUpgrade('upgradeContract', 'rocketNodeManager', rocketMinipoolManagerNew.abi, rocketMinipoolManagerNew.address, {
from: userOne,
}), 'Random address upgraded a contract', 'Account is not a temporary guardian');
});
it(printTitle('guardian', 'cannot upgrade a contract with an invalid address'), async () => {
await shouldRevert(setDaoNodeTrustedBootstrapUpgrade('upgradeContract', 'rocketNodeManager', rocketMinipoolManagerNew.abi, '0x0000000000000000000000000000000000000000', {
from: guardian,
}), 'Guardian upgraded a contract with an in