@nodeset/contracts
Version:
Protocol for accessing NodeSet's Constellation Ethereum staking network
1,021 lines (834 loc) • 49.5 kB
JavaScript
import {
RocketDAOProtocolSettingsMinipool,
RocketDAOProtocolSettingsNetwork,
RocketMinipoolManager,
RevertOnTransfer,
RocketVault,
RocketTokenRPL,
RocketDAONodeTrustedSettingsMinipool,
RocketMinipoolBase,
RocketMinipoolBondReducer,
RocketDAOProtocolSettingsRewards, RocketNodeManager, RocketMinipoolDelegate, RocketNodeDistributorFactory,
} from '../_utils/artifacts';
import { increaseTime } from '../_utils/evm';
import { printTitle } from '../_utils/formatting';
import { shouldRevert } from '../_utils/testing';
import { userDeposit } from '../_helpers/deposit';
import {
getMinipoolMinimumRPLStake,
createMinipool,
stakeMinipool,
dissolveMinipool,
getNodeActiveMinipoolCount, promoteMinipool, minipoolStates,
} from '../_helpers/minipool';
import {
registerNode,
setNodeTrusted,
setNodeWithdrawalAddress,
nodeStakeRPL,
getNodeAverageFee,
} from '../_helpers/node';
import { mintRPL } from '../_helpers/tokens';
import { close } from './scenario-close';
import { dissolve } from './scenario-dissolve';
import { refund } from './scenario-refund';
import { stake } from './scenario-stake';
import { beginUserDistribute, withdrawValidatorBalance } from './scenario-withdraw-validator-balance';
import { setDAOProtocolBootstrapSetting } from '../dao/scenario-dao-protocol-bootstrap';
import {
setDAONodeTrustedBootstrapSetting,
setDaoNodeTrustedBootstrapUpgrade
} from '../dao/scenario-dao-node-trusted-bootstrap';
import { reduceBond } from './scenario-reduce-bond';
import { assertBN } from '../_helpers/bn';
import { skimRewards } from './scenario-skim-rewards';
import { artifacts } from 'hardhat';
export default function() {
contract('RocketMinipool', async (accounts) => {
// Accounts
const [
owner,
node,
emptyNode,
nodeWithdrawalAddress,
trustedNode,
dummySwc,
random,
] = accounts;
// Setup
let launchTimeout = (60 * 60 * 72); // 72 hours
let withdrawalDelay = 20;
let scrubPeriod = (60 * 60 * 24); // 24 hours
let bondReductionWindowStart = (2 * 24 * 60 * 60);
let bondReductionWindowLength = (2 * 24 * 60 * 60);
let rewardClaimPeriodTime = (28 * 24 * 60 * 60); // 28 days
let userDistributeTime = (90 * 24 * 60 * 60); // 90 days
let initialisedMinipool;
let prelaunchMinipool;
let prelaunchMinipool2;
let stakingMinipool;
let dissolvedMinipool;
let withdrawalBalance = '36'.ether;
let newDelegateAddress = '0x0000000000000000000000000000000000000001';
let oldDelegateAddress;
const lebDepositNodeAmount = '8'.ether;
const halfDepositNodeAmount = '16'.ether;
before(async () => {
oldDelegateAddress = (await RocketMinipoolDelegate.deployed()).address;
// Register node & set withdrawal address
await registerNode({from: node});
await setNodeWithdrawalAddress(node, nodeWithdrawalAddress, {from: node});
// Register empty node
await registerNode({from: emptyNode});
// Register trusted node
await registerNode({from: trustedNode});
await setNodeTrusted(trustedNode, 'saas_1', 'node@home.com', owner);
// Set settings
await setDAOProtocolBootstrapSetting(RocketDAOProtocolSettingsMinipool, 'minipool.launch.timeout', launchTimeout, {from: owner});
await setDAOProtocolBootstrapSetting(RocketDAOProtocolSettingsMinipool, 'minipool.withdrawal.delay', withdrawalDelay, {from: owner});
await setDAONodeTrustedBootstrapSetting(RocketDAONodeTrustedSettingsMinipool, 'minipool.scrub.period', scrubPeriod, {from: owner});
await setDAONodeTrustedBootstrapSetting(RocketDAONodeTrustedSettingsMinipool, 'minipool.bond.reduction.window.start', bondReductionWindowStart, {from: owner});
await setDAONodeTrustedBootstrapSetting(RocketDAONodeTrustedSettingsMinipool, 'minipool.bond.reduction.window.length', bondReductionWindowLength, {from: owner});
await setDAOProtocolBootstrapSetting(RocketDAOProtocolSettingsRewards, 'rpl.rewards.claim.period.time', rewardClaimPeriodTime, {from: owner});
// Set rETH collateralisation target to a value high enough it won't cause excess ETH to be funneled back into deposit pool and mess with our calcs
await setDAOProtocolBootstrapSetting(RocketDAOProtocolSettingsNetwork, 'network.reth.collateral.target', '50'.ether, {from: owner});
// Set user distribute time
await setDAOProtocolBootstrapSetting(RocketDAOProtocolSettingsMinipool, 'minipool.user.distribute.window.start', userDistributeTime, {from: owner});
// Stake RPL to cover minipools
let minipoolRplStake = await getMinipoolMinimumRPLStake();
let rplStake = minipoolRplStake.mul('7'.BN);
await mintRPL(owner, node, rplStake);
await nodeStakeRPL(rplStake, {from: node});
// Create a dissolved minipool
await userDeposit({ from: random, value: '16'.ether, });
dissolvedMinipool = await createMinipool({from: node, value: '16'.ether});
await increaseTime(web3, launchTimeout + 1);
await dissolveMinipool(dissolvedMinipool, {from: node});
// Create minipools
await userDeposit({ from: random, value: '46'.ether, });
prelaunchMinipool = await createMinipool({from: node, value: '16'.ether});
prelaunchMinipool2 = await createMinipool({from: node, value: '16'.ether});
stakingMinipool = await createMinipool({from: node, value: '16'.ether});
initialisedMinipool = await createMinipool({from: node, value: '16'.ether});
// Wait required scrub period
await increaseTime(web3, scrubPeriod + 1);
// Progress minipools into desired statuses
await stakeMinipool(stakingMinipool, {from: node});
// Check minipool statuses
let initialisedStatus = await initialisedMinipool.getStatus.call();
let prelaunchStatus = await prelaunchMinipool.getStatus.call();
let prelaunch2Status = await prelaunchMinipool2.getStatus.call();
let stakingStatus = await stakingMinipool.getStatus.call();
let dissolvedStatus = await dissolvedMinipool.getStatus.call();
assertBN.equal(initialisedStatus, minipoolStates.Initialised, 'Incorrect initialised minipool status');
assertBN.equal(prelaunchStatus, minipoolStates.Prelaunch, 'Incorrect prelaunch minipool status');
assertBN.equal(prelaunch2Status, minipoolStates.Prelaunch, 'Incorrect prelaunch minipool status');
assertBN.equal(stakingStatus, minipoolStates.Staking, 'Incorrect staking minipool status');
assertBN.equal(dissolvedStatus, minipoolStates.Dissolved, 'Incorrect dissolved minipool status');
});
async function upgradeNetworkDelegateContract() {
// Upgrade the delegate contract
await setDaoNodeTrustedBootstrapUpgrade('upgradeContract', 'rocketMinipoolDelegate', [], newDelegateAddress, {
from: owner,
});
// Check effective delegate is still the original
const minipool = await RocketMinipoolBase.at(stakingMinipool.address);
const effectiveDelegate = await minipool.getEffectiveDelegate.call()
assert.notEqual(effectiveDelegate, newDelegateAddress, "Effective delegate was updated")
}
async function resetNetworkDelegateContract() {
// Upgrade the delegate contract
await setDaoNodeTrustedBootstrapUpgrade('upgradeContract', 'rocketMinipoolDelegate', [], oldDelegateAddress, {
from: owner,
});
}
//
// General
//
it(printTitle('random address', 'cannot send ETH to non-payable minipool delegate methods'), async () => {
// Attempt to send ETH to view method
await shouldRevert(prelaunchMinipool.getStatus({
from: random,
value: '1'.ether,
}), 'Sent ETH to a non-payable minipool delegate view method');
// Attempt to send ETH to mutator method
await shouldRevert(refund(prelaunchMinipool, {
from: node,
value: '1'.ether,
}), 'Sent ETH to a non-payable minipool delegate mutator method');
});
it(printTitle('minipool', 'has correct withdrawal credentials'), async () => {
// Get contracts
const rocketMinipoolManager = await RocketMinipoolManager.deployed()
// Withdrawal credentials settings
const withdrawalPrefix = '01';
const padding = '0000000000000000000000';
// Get minipool withdrawal credentials
let withdrawalCredentials = await rocketMinipoolManager.getMinipoolWithdrawalCredentials.call(initialisedMinipool.address);
// Check withdrawal credentials
let expectedWithdrawalCredentials = ('0x' + withdrawalPrefix + padding + initialisedMinipool.address.substr(2));
assert.equal(withdrawalCredentials.toLowerCase(), expectedWithdrawalCredentials.toLowerCase(), 'Invalid minipool withdrawal credentials');
});
it(printTitle('node operator', 'cannot create a minipool if network capacity is reached and destroying a minipool reduces the capacity'), async () => {
// Retrieve the current number of minipools
const rocketMinipoolManager = await RocketMinipoolManager.deployed();
const minipoolCount = (await rocketMinipoolManager.getMinipoolCount()).toNumber();
// Set max to the current number
await setDAOProtocolBootstrapSetting(RocketDAOProtocolSettingsMinipool, 'minipool.maximum.count', minipoolCount, {from: owner});
// Creating minipool should fail now
await shouldRevert(createMinipool({from: node, value: '16'.ether}), 'Was able to create a minipool when capacity is reached', 'Global minipool limit reached');
// Destroy a pool
await withdrawValidatorBalance(stakingMinipool, withdrawalBalance, nodeWithdrawalAddress, true);
// Creating minipool should no longer fail
await createMinipool({from: node, value: '16'.ether});
});
it(printTitle('node operator', 'cannot create a minipool if delegate address is set to a non-contract'), async () => {
// Upgrade network delegate contract to random address
await upgradeNetworkDelegateContract();
// Creating minipool should fail now
await shouldRevert(createMinipool({from: node, value: '16'.ether}), 'Was able to create a minipool with bad delegate address', 'Delegate contract does not exist');
});
it(printTitle('node operator', 'cannot delegatecall to a delgate address that is a non-contract'), async () => {
// Creating minipool should fail now
let newMinipool = await createMinipool({from: node, value: '16'.ether});
const newMinipoolBase = await RocketMinipoolBase.at(newMinipool.address);
// Upgrade network delegate contract to random address
await upgradeNetworkDelegateContract();
// Call upgrade delegate
await newMinipoolBase.setUseLatestDelegate(true, {from: node})
// Staking should fail now
await shouldRevert(stakeMinipool(newMinipool, {from: node}), 'Was able to create a minipool with bad delegate address', 'Delegate contract does not exist');
// Reset the delegate to working contract to prevent invariant tests from failing
await resetNetworkDelegateContract();
});
//
// Finalise
//
it(printTitle('node operator', 'can finalise a user withdrawn minipool'), async () => {
// Send enough ETH to allow distribution
await web3.eth.sendTransaction({
from: owner,
to: stakingMinipool.address,
value: withdrawalBalance
});
// Begin user distribution process
await beginUserDistribute(stakingMinipool, {from: random});
// Wait 14 days
await increaseTime(web3, userDistributeTime + 1)
// Withdraw without finalising
await withdrawValidatorBalance(stakingMinipool, withdrawalBalance, random);
// Get number of active minipools before
const count1 = await getNodeActiveMinipoolCount(node);
// Finalise
await stakingMinipool.finalise({ from: nodeWithdrawalAddress });
// Get number of active minipools after
const count2 = await getNodeActiveMinipoolCount(node);
// Make sure active minipool count reduced by one
assertBN.equal(count1.sub(count2), 1, "Active minipools did not decrement by 1");
});
it(printTitle('node operator', 'cannot finalise a withdrawn minipool twice'), async () => {
// Send enough ETH to allow distribution
await web3.eth.sendTransaction({
from: owner,
to: stakingMinipool.address,
value: withdrawalBalance
});
// Begin user distribution process
await beginUserDistribute(stakingMinipool, {from: random});
// Wait 14 days
await increaseTime(web3, userDistributeTime + 1)
// Withdraw without finalising
await withdrawValidatorBalance(stakingMinipool, withdrawalBalance, random);
// Finalise
await stakingMinipool.finalise({ from: nodeWithdrawalAddress });
// Second time should fail
await shouldRevert(stakingMinipool.finalise({ from: nodeWithdrawalAddress }), "Was able to finalise pool twice", "Minipool has already been finalised");
});
it(printTitle('node operator', 'cannot finalise a non-withdrawn minipool'), async () => {
// Finalise
await shouldRevert(stakingMinipool.finalise({ from: nodeWithdrawalAddress }), 'Minipool was finalised before withdrawn', 'Can only manually finalise after user distribution');
});
it(printTitle('random address', 'cannot finalise a withdrawn minipool'), async () => {
// Withdraw without finalising
await withdrawValidatorBalance(stakingMinipool, withdrawalBalance, nodeWithdrawalAddress);
// Finalise
await shouldRevert(stakingMinipool.finalise({ from: random }), 'Minipool was finalised by random', 'Invalid minipool owner');
});
//
// Slash
//
it(printTitle('random address', 'can slash node operator if withdrawal balance is less than 16 ETH'), async () => {
// Stake the prelaunch minipool (it has 16 ETH user funds)
await stakeMinipool(prelaunchMinipool, {from: node});
// Send enough ETH to allow distribution
await web3.eth.sendTransaction({
from: owner,
to: prelaunchMinipool.address,
value: '8'.ether
});
// Begin user distribution process
await beginUserDistribute(prelaunchMinipool, {from: random});
// Wait 14 days
await increaseTime(web3, userDistributeTime + 1)
// Post an 8 ETH balance which should result in 8 ETH worth of RPL slashing
await withdrawValidatorBalance(prelaunchMinipool, '0'.ether, random);
// Call slash method
await prelaunchMinipool.slash({ from: random });
// Check slashed flag
const slashed = await (await RocketMinipoolManager.deployed()).getMinipoolRPLSlashed(prelaunchMinipool.address);
assert(slashed, "Slashed flag not set");
// Auction house should now have slashed 8 ETH worth of RPL (which is 800 RPL at starting price)
const rocketVault = await RocketVault.deployed();
const rocketTokenRPL = await RocketTokenRPL.deployed();
const balance = await rocketVault.balanceOfToken('rocketAuctionManager', rocketTokenRPL.address);
assertBN.equal(balance, '800'.ether);
});
it(printTitle('node operator', 'is slashed if withdraw is processed when balance is less than 16 ETH'), async () => {
// Stake the prelaunch minipool (it has 16 ETH user funds)
await stakeMinipool(prelaunchMinipool, {from: node});
// Post an 8 ETH balance which should result in 8 ETH worth of RPL slashing
await withdrawValidatorBalance(prelaunchMinipool, '8'.ether, nodeWithdrawalAddress, true);
// Check slashed flag
const slashed = await (await RocketMinipoolManager.deployed()).getMinipoolRPLSlashed(prelaunchMinipool.address);
assert(slashed, "Slashed flag not set");
// Auction house should now have slashed 8 ETH worth of RPL (which is 800 RPL at starting price)
const rocketVault = await RocketVault.deployed();
const rocketTokenRPL = await RocketTokenRPL.deployed();
const balance = await rocketVault.balanceOfToken('rocketAuctionManager', rocketTokenRPL.address);
assertBN.equal(balance, '800'.ether);
});
//
// Dissolve
//
it(printTitle('node operator', 'cannot dissolve their own staking minipools'), async () => {
// Attempt to dissolve staking minipool
await shouldRevert(dissolve(stakingMinipool, {
from: node,
}), 'Dissolved a staking minipool');
});
it(printTitle('random address', 'can dissolve a timed out minipool at prelaunch'), async () => {
// Time prelaunch minipool out
await increaseTime(web3, launchTimeout);
// Dissolve prelaunch minipool
await dissolve(prelaunchMinipool, {
from: random,
});
});
it(printTitle('random address', 'cannot dissolve a minipool which is not at prelaunch'), async () => {
// Time prelaunch minipool out
await increaseTime(web3, launchTimeout);
// Attempt to dissolve initialised minipool
await shouldRevert(dissolve(initialisedMinipool, {
from: random,
}), 'Random address dissolved a minipool which was not at prelaunch');
});
it(printTitle('random address', 'cannot dissolve a minipool which has not timed out'), async () => {
// Attempt to dissolve prelaunch minipool
await shouldRevert(dissolve(prelaunchMinipool, {
from: random,
}), 'Random address dissolved a minipool which has not timed out');
});
//
// Stake
//
it(printTitle('node operator', 'can stake a minipool at prelaunch'), async () => {
// Stake prelaunch minipool
await stake(prelaunchMinipool, null, {
from: node,
});
});
it(printTitle('node operator', 'cannot stake a minipool which is not at prelaunch'), async () => {
// Attempt to stake initialised minipool
await shouldRevert(stake(initialisedMinipool, null, {
from: node,
}), 'Staked a minipool which was not at prelaunch');
});
it(printTitle('node operator', 'cannot stake a minipool with a reused validator pubkey'), async () => {
// Load contracts
const rocketMinipoolManager = await RocketMinipoolManager.deployed();
// Get minipool validator pubkey
const validatorPubkey = await rocketMinipoolManager.getMinipoolPubkey(prelaunchMinipool.address);
// Stake prelaunch minipool
await stake(prelaunchMinipool, null, {from: node});
// Attempt to stake second prelaunch minipool with same pubkey
await shouldRevert(stake(prelaunchMinipool2, null, {
from: node,
}, validatorPubkey), 'Staked a minipool with a reused validator pubkey');
});
it(printTitle('node operator', 'cannot stake a minipool with incorrect withdrawal credentials'), async () => {
// Get withdrawal credentials
let invalidWithdrawalCredentials = '0x1111111111111111111111111111111111111111111111111111111111111111';
// Attempt to stake prelaunch minipool
await shouldRevert(stake(prelaunchMinipool, invalidWithdrawalCredentials, {
from: node,
}), 'Staked a minipool with incorrect withdrawal credentials');
});
it(printTitle('random address', 'cannot stake a minipool'), async () => {
// Attempt to stake prelaunch minipool
await shouldRevert(stake(prelaunchMinipool, null, {
from: random,
}), 'Random address staked a minipool');
});
//
// Withdraw validator balance
//
it(printTitle('random', 'random address cannot withdraw and destroy a node operators minipool balance'), async () => {
// Wait 14 days
await increaseTime(web3, 60 * 60 * 24 * 14 + 1)
// Attempt to send validator balance
await shouldRevert(withdrawValidatorBalance(stakingMinipool, withdrawalBalance, random, true), 'Random address withdrew validator balance from a node operators minipool', "Only owner can distribute right now");
});
it(printTitle('random', 'random address can trigger a payout of withdrawal balance if balance is greater than 16 ETH'), async () => {
// Send enough ETH to allow distribution
await web3.eth.sendTransaction({
from: owner,
to: stakingMinipool.address,
value: '32'.ether
});
// Begin user distribution process
await beginUserDistribute(stakingMinipool, {from: random});
// Wait 14 days
await increaseTime(web3, userDistributeTime + 1)
// Post an 8 ETH balance which should result in 8 ETH worth of RPL slashing
await withdrawValidatorBalance(stakingMinipool, '0'.ether, random);
});
it(printTitle('random', 'random address cannot trigger a payout of withdrawal balance if balance is less than 16 ETH'), async () => {
// Attempt to send validator balance
await shouldRevert(withdrawValidatorBalance(stakingMinipool, '15'.ether, random, false), 'Random address was able to execute withdraw on sub 16 ETH minipool', 'Only owner can distribute right now');
});
it(printTitle('node operator withdrawal address', 'can withdraw their ETH once it is received, then distribute ETH to the rETH contract / deposit pool and destroy the minipool'), async () => {
// Send validator balance and withdraw
await withdrawValidatorBalance(stakingMinipool, withdrawalBalance, nodeWithdrawalAddress, true);
});
it(printTitle('node operator account', 'can also withdraw their ETH once it is received, then distribute ETH to the rETH contract / deposit pool and destroy the minipool'), async () => {
// Send validator balance and withdraw
await withdrawValidatorBalance(stakingMinipool, withdrawalBalance, node, true);
});
it(printTitle('malicious node operator', 'can not prevent a payout by using a reverting contract as withdraw address'), async () => {
// Set the node's withdraw address to a reverting contract
const revertOnTransfer = await RevertOnTransfer.deployed();
await setNodeWithdrawalAddress(node, revertOnTransfer.address, {from: nodeWithdrawalAddress});
// Wait 14 days
await increaseTime(web3, 60 * 60 * 24 * 14 + 1)
// Send enough ETH to allow distribution
await web3.eth.sendTransaction({
from: owner,
to: stakingMinipool.address,
value: withdrawalBalance
});
// Begin user distribution process
await beginUserDistribute(stakingMinipool, {from: random});
// Wait 14 days
await increaseTime(web3, userDistributeTime + 1)
// Post an 8 ETH balance which should result in 8 ETH worth of RPL slashing
await withdrawValidatorBalance(stakingMinipool, '0'.ether, random);
});
it(printTitle('random address', 'can send validator balance to a withdrawable minipool in one transaction'), async () => {
await web3.eth.sendTransaction({
from: random,
to: stakingMinipool.address,
value: withdrawalBalance,
});
// Begin user distribution process
await beginUserDistribute(stakingMinipool, {from: random});
// Wait 14 days
await increaseTime(web3, userDistributeTime + 1)
// Post an 8 ETH balance which should result in 8 ETH worth of RPL slashing
await withdrawValidatorBalance(stakingMinipool, '0'.ether, random);
});
it(printTitle('random address', 'can send validator balance to a withdrawable minipool across multiple transactions'), async () => {
// Get tx amount (half of withdrawal balance)
let amount1 = withdrawalBalance.div('2'.BN);
let amount2 = withdrawalBalance.sub(amount1);
await web3.eth.sendTransaction({
from: random,
to: stakingMinipool.address,
value: amount1,
});
await web3.eth.sendTransaction({
from: owner,
to: stakingMinipool.address,
value: amount2,
});
// Begin user distribution process
await beginUserDistribute(stakingMinipool, {from: random});
// Wait 14 days
await increaseTime(web3, userDistributeTime + 1)
// Post an 8 ETH balance which should result in 8 ETH worth of RPL slashing
await withdrawValidatorBalance(stakingMinipool, '0'.ether, random);
});
//
// Skim rewards
//
it(printTitle('node operator', 'can skim rewards less than 8 ETH'), async () => {
// Send 1 ETH to the minipool
await web3.eth.sendTransaction({
from: owner,
to: stakingMinipool.address,
value: '1'.ether,
});
// Skim rewards from node
await skimRewards(stakingMinipool, {from: node});
});
it(printTitle('random user', 'can skim rewards less than 8 ETH'), async () => {
// Send 1 ETH to the minipool
await web3.eth.sendTransaction({
from: owner,
to: stakingMinipool.address,
value: '1'.ether,
});
// Skim rewards from node
await skimRewards(stakingMinipool, {from: random});
});
it(printTitle('random user', 'can skim rewards less than 8 ETH twice'), async () => {
// Send 1 ETH to the minipool
await web3.eth.sendTransaction({
from: owner,
to: stakingMinipool.address,
value: '1'.ether,
});
// Skim rewards from random
await skimRewards(stakingMinipool, {from: random});
// Send 1 ETH to the minipool
await web3.eth.sendTransaction({
from: owner,
to: stakingMinipool.address,
value: '1'.ether,
});
// Skim rewards from random
await skimRewards(stakingMinipool, {from: random});
});
it(printTitle('random user + node operator', 'can skim rewards less than 8 ETH twice interchangeably'), async () => {
// Send 1 ETH to the minipool
await web3.eth.sendTransaction({
from: owner,
to: stakingMinipool.address,
value: '1.5'.ether,
});
// Skim rewards from random
await skimRewards(stakingMinipool, {from: random});
// Send 1 ETH to the minipool
await web3.eth.sendTransaction({
from: owner,
to: stakingMinipool.address,
value: '2'.ether,
});
// Skim rewards from node
await skimRewards(stakingMinipool, {from: node});
});
//
// Close
//
it(printTitle('node operator', 'can close a dissolved minipool'), async () => {
// Send 16 ETH to minipool
await web3.eth.sendTransaction({
from: random,
to: dissolvedMinipool.address,
value: '16'.ether,
});
// Close dissolved minipool
await close(dissolvedMinipool, {
from: node,
});
});
it(printTitle('node operator', 'cannot close a minipool which is not dissolved'), async () => {
// Attempt to close staking minipool
await shouldRevert(close(stakingMinipool, {
from: node,
}), 'Closed a minipool which was not dissolved', 'The minipool can only be closed while dissolved');
});
it(printTitle('random address', 'cannot close a dissolved minipool'), async () => {
// Attempt to close dissolved minipool
await shouldRevert(close(dissolvedMinipool, {
from: random,
}), 'Random address closed a minipool', 'Invalid minipool owner');
});
//
// Delegate upgrades
//
it(printTitle('node operator', 'can upgrade and rollback their delegate contract'), async () => {
await upgradeNetworkDelegateContract();
// Get contract
const minipool = await RocketMinipoolBase.at(stakingMinipool.address);
// Store original delegate
let originalDelegate = await minipool.getEffectiveDelegate.call();
// Call upgrade delegate
await minipool.delegateUpgrade({from: node});
// Check delegate settings
let effectiveDelegate = await minipool.getEffectiveDelegate.call();
let previousDelegate = await minipool.getPreviousDelegate.call();
assert.strictEqual(effectiveDelegate, newDelegateAddress, "Effective delegate was not updated");
assert.strictEqual(previousDelegate, originalDelegate, "Previous delegate was not updated");
// Call upgrade rollback
await minipool.delegateRollback({from: node});
// Check effective delegate
effectiveDelegate = await minipool.getEffectiveDelegate.call();
assert.strictEqual(effectiveDelegate, originalDelegate, "Effective delegate was not rolled back");
});
it(printTitle('node operator', 'can use latest delegate contract'), async () => {
await upgradeNetworkDelegateContract();
// Get contract
const minipool = await RocketMinipoolBase.at(stakingMinipool.address);
// Store original delegate
let originalDelegate = await minipool.getEffectiveDelegate.call()
// Call upgrade delegate
await minipool.setUseLatestDelegate(true, {from: node})
let useLatest = await minipool.getUseLatestDelegate.call()
assert.isTrue(useLatest, "Use latest flag was not set")
// Check delegate settings
let effectiveDelegate = await minipool.getEffectiveDelegate.call()
let currentDelegate = await minipool.getDelegate.call()
assert.strictEqual(effectiveDelegate, newDelegateAddress, "Effective delegate was not updated")
assert.strictEqual(currentDelegate, originalDelegate, "Current delegate was updated")
// Upgrade the delegate contract again
newDelegateAddress = '0x0000000000000000000000000000000000000002'
await setDaoNodeTrustedBootstrapUpgrade('upgradeContract', 'rocketMinipoolDelegate', [], newDelegateAddress, {
from: owner,
});
// Check effective delegate
effectiveDelegate = await minipool.getEffectiveDelegate.call();
assert.strictEqual(effectiveDelegate, newDelegateAddress, "Effective delegate was not updated");
// Reset the delegate to working contract to prevent invariant tests from failing
await resetNetworkDelegateContract();
});
it(printTitle('random', 'cannot upgrade, rollback or set use latest delegate contract'), async () => {
await upgradeNetworkDelegateContract();
// Get contract
const minipool = await RocketMinipoolBase.at(stakingMinipool.address);
// Call upgrade delegate from random
await shouldRevert(minipool.delegateUpgrade({from: random}), "Random was able to upgrade delegate", "Only the node operator can access this method");
// Call upgrade delegate from node
await minipool.delegateUpgrade({from: node});
// Call upgrade rollback from random
await shouldRevert(minipool.delegateRollback({from: random}), "Random was able to rollback delegate", "Only the node operator can access this method") ;
// Call set use latest from random
await shouldRevert(minipool.setUseLatestDelegate(true, {from: random}), "Random was able to set use latest delegate", "Only the node operator can access this method") ;
// Reset the delegate to working contract to prevent invariant tests from failing
await resetNetworkDelegateContract();
await minipool.delegateUpgrade({from: node});
});
//
// Reducing bond amount
//
it(printTitle('node operator', 'can reduce bond amount to a valid deposit amount'), async () => {
// Get contracts
const rocketMinipoolBondReducer = await RocketMinipoolBondReducer.deployed();
// Signal wanting to reduce
await rocketMinipoolBondReducer.beginReduceBondAmount(stakingMinipool.address, '8'.ether, {from: node});
await increaseTime(web3, bondReductionWindowStart + 1);
// Reduction from 16 ETH to 8 ETH should be valid
await reduceBond(stakingMinipool, {from: node});
});
it(printTitle('node operator', 'average node fee gets updated correctly on bond reduction'), async () => {
// Get contracts
const rocketNodeManager = await RocketNodeManager.deployed();
// Set the network node fee to 20%
await setDAOProtocolBootstrapSetting(RocketDAOProtocolSettingsNetwork, 'network.node.fee.minimum', '0.20'.ether, {from: owner});
await setDAOProtocolBootstrapSetting(RocketDAOProtocolSettingsNetwork, 'network.node.fee.target', '0.20'.ether, {from: owner});
await setDAOProtocolBootstrapSetting(RocketDAOProtocolSettingsNetwork, 'network.node.fee.maximum', '0.20'.ether, {from: owner});
// Stake RPL to cover a 16 ETH and an 8 ETH minipool (1.6 + 2.4)
let rplStake = '400'.ether
await mintRPL(owner, emptyNode, rplStake);
await nodeStakeRPL(rplStake, {from: emptyNode});
// Deposit enough user funds to cover minipool creation
await userDeposit({ from: random, value: '64'.ether, });
// Create the minipools
let minipool1 = await createMinipool({from: emptyNode, value: '16'.ether});
let minipool2 = await createMinipool({from: emptyNode, value: '16'.ether});
// Wait required scrub period
await increaseTime(web3, scrubPeriod + 1);
// Progress minipools into desired statuses
await stakeMinipool(minipool1, {from: emptyNode});
await stakeMinipool(minipool2, {from: emptyNode});
// Set the network node fee to 10%
await setDAOProtocolBootstrapSetting(RocketDAOProtocolSettingsNetwork, 'network.node.fee.minimum', '0.10'.ether, {from: owner});
await setDAOProtocolBootstrapSetting(RocketDAOProtocolSettingsNetwork, 'network.node.fee.target', '0.10'.ether, {from: owner});
await setDAOProtocolBootstrapSetting(RocketDAOProtocolSettingsNetwork, 'network.node.fee.maximum', '0.10'.ether, {from: owner});
// Get contracts
const rocketMinipoolBondReducer = await RocketMinipoolBondReducer.deployed();
// Signal wanting to reduce
await rocketMinipoolBondReducer.beginReduceBondAmount(minipool1.address, '8'.ether, {from: emptyNode});
await increaseTime(web3, bondReductionWindowStart + 1);
// Reduction from 16 ETH to 8 ETH should be valid
let fee1 = await rocketNodeManager.getAverageNodeFee(emptyNode);
await reduceBond(minipool1, {from: emptyNode});
let fee2 = await rocketNodeManager.getAverageNodeFee(emptyNode);
/*
Node operator now has 1x 16 ETH bonded minipool at 20% node fee and 1x 8 ETH bonded minipool at 10% fee
Before bond reduction average node fee should be 20%, weighted average node fee after should be 14%
*/
assertBN.equal(fee1, '0.20'.ether, 'Incorrect node fee');
assertBN.equal(fee2, '0.14'.ether, 'Incorrect node fee');
});
it(printTitle('node operator', 'can reduce bond amount to a valid deposit amount after reward period'), async () => {
// Upgrade RocketNodeDeposit to add 4 ETH LEB support
const RocketNodeDepositLEB4 = artifacts.require('RocketNodeDepositLEB4.sol');
const rocketNodeDepositLEB4 = await RocketNodeDepositLEB4.deployed();
await setDaoNodeTrustedBootstrapUpgrade("upgradeContract", "rocketNodeDeposit", RocketNodeDepositLEB4.abi, rocketNodeDepositLEB4.address, {from: owner});
// Get contracts
const rocketMinipoolBondReducer = await RocketMinipoolBondReducer.deployed();
// Signal wanting to reduce
await rocketMinipoolBondReducer.beginReduceBondAmount(stakingMinipool.address, '8'.ether, {from: node});
await increaseTime(web3, bondReductionWindowStart + 1);
// Reduction from 16 ETH to 8 ETH should be valid
await reduceBond(stakingMinipool, {from: node});
// Increase
await increaseTime(web3, rewardClaimPeriodTime + 1);
// Signal wanting to reduce again
await rocketMinipoolBondReducer.beginReduceBondAmount(stakingMinipool.address, '4'.ether, {from: node});
await increaseTime(web3, bondReductionWindowStart + 1);
// Reduction from 16 ETH to 8 ETH should be valid
await reduceBond(stakingMinipool, {from: node});
});
it(printTitle('node operator', 'can not reduce bond amount to a valid deposit amount within reward period'), async () => {
// Upgrade RocketNodeDeposit to add 4 ETH LEB support
const RocketNodeDepositLEB4 = artifacts.require('RocketNodeDepositLEB4.sol');
const rocketNodeDepositLEB4 = await RocketNodeDepositLEB4.deployed();
await setDaoNodeTrustedBootstrapUpgrade("upgradeContract", "rocketNodeDeposit", RocketNodeDepositLEB4.abi, rocketNodeDepositLEB4.address, {from: owner});
// Get contracts
const rocketMinipoolBondReducer = await RocketMinipoolBondReducer.deployed();
// Signal wanting to reduce
await rocketMinipoolBondReducer.beginReduceBondAmount(stakingMinipool.address, '8'.ether, {from: node});
await increaseTime(web3, bondReductionWindowStart + 1);
// Reduction from 16 ETH to 8 ETH should be valid
await reduceBond(stakingMinipool, {from: node});
// Signal wanting to reduce again
await shouldRevert(rocketMinipoolBondReducer.beginReduceBondAmount(stakingMinipool.address, '4'.ether, {from: node}), 'Was able to reduce without waiting', 'Not enough time has passed since last bond reduction');
});
it(printTitle('node operator', 'cannot reduce bond without waiting'), async () => {
// Get contracts
const rocketMinipoolBondReducer = await RocketMinipoolBondReducer.deployed();
// Signal wanting to reduce and wait 7 days
await rocketMinipoolBondReducer.beginReduceBondAmount(stakingMinipool.address, '8'.ether, {from: node});
// Reduction from 16 ETH to 8 ETH should be valid
await shouldRevert(reduceBond(stakingMinipool, {from: node}), 'Was able to reduce bond without waiting', 'Wait period not satisfied');
});
it(printTitle('node operator', 'cannot begin to reduce bond after odao has cancelled'), async () => {
// Get contracts
const rocketMinipoolBondReducer = await RocketMinipoolBondReducer.deployed();
// Vote to cancel
await rocketMinipoolBondReducer.voteCancelReduction(stakingMinipool.address, {from: trustedNode});
// Signal wanting to reduce and wait 7 days
await shouldRevert(rocketMinipoolBondReducer.beginReduceBondAmount(stakingMinipool.address, '8'.ether, {from: node}), 'Was able to begin to reduce bond', 'This minipool is not allowed to reduce bond');
});
it(printTitle('node operator', 'cannot reduce bond after odao has cancelled'), async () => {
// Get contracts
const rocketMinipoolBondReducer = await RocketMinipoolBondReducer.deployed();
// Signal wanting to reduce and wait 7 days
await rocketMinipoolBondReducer.beginReduceBondAmount(stakingMinipool.address, '8'.ether, {from: node});
await increaseTime(web3, bondReductionWindowStart + 1);
// Vote to cancel
await rocketMinipoolBondReducer.voteCancelReduction(stakingMinipool.address, {from: trustedNode});
// Wait and try to reduce
await shouldRevert(reduceBond(stakingMinipool, {from: node}), 'Was able to reduce bond after it was cancelled', 'This minipool is not allowed to reduce bond');
});
it(printTitle('node operator', 'cannot reduce bond if wait period exceeds the limit'), async () => {
// Get contracts
const rocketMinipoolBondReducer = await RocketMinipoolBondReducer.deployed();
// Signal wanting to reduce and wait 7 days
await rocketMinipoolBondReducer.beginReduceBondAmount(stakingMinipool.address, '8'.ether, {from: node});
await increaseTime(web3, bondReductionWindowStart + bondReductionWindowLength + 1);
// Reduction from 16 ETH to 8 ETH should be valid
await shouldRevert(reduceBond(stakingMinipool, {from: node}), 'Was able to reduce bond without waiting', 'Wait period not satisfied');
});
it(printTitle('node operator', 'cannot reduce bond without beginning the process first'), async () => {
// Reduction from 16 ETH to 8 ETH should be valid
await shouldRevert(reduceBond(stakingMinipool, {from: node}), 'Was able to reduce bond without beginning the process', 'Wait period not satisfied');
});
it(printTitle('node operator', 'cannot reduce bond amount to an invalid deposit amount'), async () => {
// Get contracts
const rocketMinipoolBondReducer = await RocketMinipoolBondReducer.deployed();
// Reduce to 9 ether bond should fail
await shouldRevert(rocketMinipoolBondReducer.beginReduceBondAmount(stakingMinipool.address, '9'.ether, {from: node}), 'Was able to reduce to invalid bond', 'Invalid bond amount');
});
it(printTitle('node operator', 'cannot increase bond amount'), async () => {
// Get contracts
const rocketMinipoolBondReducer = await RocketMinipoolBondReducer.deployed();
// Signal wanting to reduce and wait 7 days
await shouldRevert(rocketMinipoolBondReducer.beginReduceBondAmount(stakingMinipool.address, '18'.ether, {from: node}), 'Was able to increase bond', 'Invalid bond amount');
});
it(printTitle('node operator', 'cannot reduce bond amount while in invalid state'), async () => {
// Get contracts
const rocketMinipoolBondReducer = await RocketMinipoolBondReducer.deployed();
// Signal wanting to reduce and wait 7 days
await shouldRevert(rocketMinipoolBondReducer.beginReduceBondAmount(prelaunchMinipool.address, '8'.ether, {from: node}), 'Was able to begin reducing bond on a prelaunch minipool', 'Minipool must be staking');
await shouldRevert(rocketMinipoolBondReducer.beginReduceBondAmount(initialisedMinipool.address, '8'.ether, {from: node}), 'Was able to reduce bond on an initialised minipool', 'Minipool must be staking');
await increaseTime(web3, bondReductionWindowStart + 1);
});
//
// Misc checks
//
it(printTitle('node operator', 'cannot promote a non-vacant minipool'), async () => {
// Try to promote (and fail)
await shouldRevert(promoteMinipool(prelaunchMinipool, {from: node}), 'Was able to promote non-vacant minipool', 'Cannot promote a non-vacant minipool');
await shouldRevert(promoteMinipool(stakingMinipool, {from: node}), 'Was able to promote non-vacant minipool', 'The minipool can only promote while in prelaunch');
await shouldRevert(promoteMinipool(initialisedMinipool, {from: node}), 'Was able to promote non-vacant minipool', 'The minipool can only promote while in prelaunch');
await shouldRevert(promoteMinipool(dissolvedMinipool, {from: node}), 'Was able to promote non-vacant minipool', 'The minipool can only promote while in prelaunch');
});
const average_fee_tests = [
[
{
fee: '0.10',
amount: lebDepositNodeAmount,
expectedFee: '0.10'
},
{
fee: '0.10',
amount: lebDepositNodeAmount,
expectedFee: '0.10'
},
{
fee: '0.10',
amount: halfDepositNodeAmount,
expectedFee: '0.10'
},
],
[
{
fee: '0.10',
amount: halfDepositNodeAmount,
expectedFee: '0.10'
},
{
fee: '0.20',
amount: lebDepositNodeAmount,
expectedFee: '0.16'
},
{
fee: '0.20',
amount: lebDepositNodeAmount,
expectedFee: '0.175'
},
],
]
for (let i = 0; i < average_fee_tests.length; i++) {
let test = average_fee_tests[i];
it(printTitle('node operator', 'has correct average node fee #' + (i+1)), async () => {
async function setNetworkNodeFee(fee) {
await setDAOProtocolBootstrapSetting(RocketDAOProtocolSettingsNetwork, 'network.node.fee.minimum', fee, {from: owner});
await setDAOProtocolBootstrapSetting(RocketDAOProtocolSettingsNetwork, 'network.node.fee.target', fee, {from: owner});
await setDAOProtocolBootstrapSetting(RocketDAOProtocolSettingsNetwork, 'network.node.fee.maximum', fee, {from: owner});
}
// Stake RPL to cover minipools
let minipoolRplStake = await getMinipoolMinimumRPLStake();
let rplStake = minipoolRplStake.mul('10'.BN);
await mintRPL(owner, emptyNode, rplStake);
await nodeStakeRPL(rplStake, {from: emptyNode});
for (const step of test) {
// Set fee to 10%
await setNetworkNodeFee(web3.utils.toWei(step.fee, 'ether'));
// Deposit
let minipool = await createMinipool({from: emptyNode, value: step.amount});
await userDeposit({ from: random, value: '32'.ether, });
// Wait required scrub period
await increaseTime(web3, scrubPeriod + 1);
// Progress minipools into desired statuses
await stakeMinipool(minipool, {from: emptyNode});
// Get average
let average = await getNodeAverageFee(emptyNode);
assertBN.equal(average, web3.utils.toWei(step.expectedFee, 'ether'), "Invalid average fee");
}
});
}
});
}