@nodeset/contracts
Version:
Protocol for accessing NodeSet's Constellation Ethereum staking network
284 lines (227 loc) • 13.7 kB
JavaScript
import {
RocketDAOProtocolSettingsMinipool,
RocketDAOProtocolSettingsNetwork,
RocketMinipoolPenalty,
RocketStorage,
PenaltyTest,
RocketNodeStaking, RocketDAONodeTrustedSettingsMinipool,
} from '../_utils/artifacts';
import { printTitle } from '../_utils/formatting';
import { shouldRevert } from '../_utils/testing';
import { userDeposit } from '../_helpers/deposit';
import { getMinipoolMinimumRPLStake, createMinipool, stakeMinipool } from '../_helpers/minipool';
import { registerNode, setNodeTrusted, setNodeWithdrawalAddress, nodeStakeRPL } from '../_helpers/node';
import { mintRPL } from '../_helpers/tokens';
import { beginUserDistribute, withdrawValidatorBalance } from './scenario-withdraw-validator-balance';
import { setDAOProtocolBootstrapSetting } from '../dao/scenario-dao-protocol-bootstrap';
import { increaseTime, mineBlocks } from '../_utils/evm'
import {
setDAONodeTrustedBootstrapSetting,
setDaoNodeTrustedBootstrapUpgrade
} from '../dao/scenario-dao-node-trusted-bootstrap';
import { submitPrices } from '../_helpers/network';
import { assertBN } from '../_helpers/bn';
export default function() {
contract('RocketMinipool', async (accounts) => {
// Accounts
const [
owner,
node,
nodeWithdrawalAddress,
trustedNode,
random,
] = accounts;
// Setup
let launchTimeout = (60 * 60 * 72); // 72 hours
let withdrawalDelay = 20;
let scrubPeriod = (60 * 60 * 24); // 24 hours
let minipool;
let maxPenaltyRate = '0.5'.ether;
let penaltyTestContract;
let userDistributeStartTime = (60 * 60 * 72);
let userDistributeLength = (60 * 60);
before(async () => {
// Register node & set withdrawal address
await registerNode({from: node});
await setNodeWithdrawalAddress(node, nodeWithdrawalAddress, {from: node});
// 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 setDAOProtocolBootstrapSetting(RocketDAOProtocolSettingsMinipool, 'minipool.user.distribute.window.start', userDistributeStartTime, {from: owner});
await setDAOProtocolBootstrapSetting(RocketDAOProtocolSettingsMinipool, 'minipool.user.distribute.window.length', userDistributeLength, {from: owner});
await setDAONodeTrustedBootstrapSetting(RocketDAONodeTrustedSettingsMinipool, 'minipool.scrub.period', scrubPeriod, {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 RPL price
let block = await web3.eth.getBlockNumber();
await submitPrices(block, '1'.ether, {from: trustedNode});
// Add penalty helper contract
const rocketStorage = await RocketStorage.deployed();
penaltyTestContract = await PenaltyTest.new(rocketStorage.address, {from: owner});
await setDaoNodeTrustedBootstrapUpgrade('addContract', 'rocketPenaltyTest', penaltyTestContract.abi, penaltyTestContract.address, {
from: owner,
});
// Enable penalties
const rocketMinipoolPenalty = await RocketMinipoolPenalty.deployed();
await rocketMinipoolPenalty.setMaxPenaltyRate(maxPenaltyRate, {from: owner})
// Hard code fee to 50%
const fee = '0.5'.ether;
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});
// Deposit some user funds to assign to pools
let userDepositAmount = '16'.ether;
await userDeposit({from: random, value: userDepositAmount});
// Stake RPL to cover minipools
let minipoolRplStake = await getMinipoolMinimumRPLStake();
let rplStake = minipoolRplStake.mul('3'.BN);
await mintRPL(owner, node, rplStake);
await nodeStakeRPL(rplStake, {from: node});
await mintRPL(owner, trustedNode, rplStake);
await nodeStakeRPL(rplStake, {from: trustedNode});
// Create minipools
minipool = await createMinipool({from: node, value: '16'.ether});
// Wait required scrub period
await increaseTime(web3, scrubPeriod + 1);
// Stake minipools
await stakeMinipool(minipool, {from: node});
});
async function withdrawAndCheck(minipool, withdrawalBalance, from, finalise, expectedUser, expectedNode, userDistribute = false) {
const withdrawalBalanceBN = withdrawalBalance.ether;
const expectedUserBN = expectedUser.ether;
const expectedNodeBN = expectedNode.ether;
let result
if (userDistribute) {
// Send ETH to minipool
await web3.eth.sendTransaction({
from: from,
to: minipool.address,
value: withdrawalBalanceBN
});
// Begin user distribution process
await beginUserDistribute(minipool, {from});
// Wait 14 days
await increaseTime(web3, userDistributeStartTime + 1)
// Process withdrawal
result = await withdrawValidatorBalance(minipool, '0'.ether, from, finalise);
} else {
// Process withdrawal
result = await withdrawValidatorBalance(minipool, withdrawalBalanceBN, from, finalise);
}
// Check results
assertBN.equal(expectedUserBN, result.rethBalanceChange, "User balance was incorrect");
assertBN.equal(expectedNodeBN, result.nodeBalanceChange, "Node balance was incorrect");
}
async function slashAndCheck(from, expectedSlash) {
// Get contracts
const rocketNodeStaking = await RocketNodeStaking.deployed()
const rplStake1 = await rocketNodeStaking.getNodeRPLStake(node)
await minipool.slash({from: from})
const rplStake2 = await rocketNodeStaking.getNodeRPLStake(node)
const slashedAmount = rplStake1.sub(rplStake2)
assertBN.equal(expectedSlash, slashedAmount, 'Slashed amount was incorrect')
}
it(printTitle('node operator withdrawal address', 'can process withdrawal when balance is greater than 32 ETH and not marked as withdrawable'), async () => {
// Process withdraw
await withdrawAndCheck(minipool, '36', nodeWithdrawalAddress, false, '17', '19');
});
it(printTitle('random user', 'can process withdrawal when balance is greater than 32 ETH and not marked as withdrawable'), async () => {
// Process withdraw
await withdrawAndCheck(minipool, '36', random, false, '17', '19', true);
});
it(printTitle('node operator withdrawal address', 'can process withdrawal when balance is greater than 16 ETH and less than 32 ETH'), async () => {
// Process withdraw
await withdrawAndCheck(minipool, '28', nodeWithdrawalAddress, true, '16', '12');
});
it(printTitle('random user', 'can process withdrawal when balance is greater than 16 ETH and less than 32 ETH'), async () => {
// Wait 14 days
await increaseTime(web3, userDistributeStartTime + 1)
// Process withdraw
await withdrawAndCheck(minipool, '28', random, false, '16', '12', true);
});
it(printTitle('node operator withdrawal address', 'can process withdrawal when balance is greater than 16 ETH, less than 32 ETH and not marked as withdrawable'), async () => {
// Process withdraw
await withdrawAndCheck(minipool, '28', nodeWithdrawalAddress, false, '16', '12');
});
it(printTitle('random user', 'can process withdrawal when balance is greater than 16 ETH, less than 32 ETH and not marked as withdrawable'), async () => {
// Process withdraw
await withdrawAndCheck(minipool, '28', random, false, '16', '12', true);
});
it(printTitle('random user', 'can not begin user distribution without waiting for window to pass'), async () => {
// Send ETH to minipool
await web3.eth.sendTransaction({
from: random,
to: minipool.address,
value: '32'.ether
});
await beginUserDistribute(minipool, {from: random});
await shouldRevert(beginUserDistribute(minipool, {from: random}), 'Was able to begin user distribution again', 'User distribution already pending');
});
it(printTitle('random user', 'can begin user distribution after window has passed'), async () => {
// Send ETH to minipool
await web3.eth.sendTransaction({
from: random,
to: minipool.address,
value: '32'.ether
});
await beginUserDistribute(minipool, {from: random});
await increaseTime(web3, userDistributeLength + userDistributeStartTime + 1)
await beginUserDistribute(minipool, {from: random});
});
it(printTitle('node operator withdrawal address', 'can process withdrawal when balance is less than 16 ETH'), async () => {
// Process withdraw
await withdrawAndCheck(minipool, '15', nodeWithdrawalAddress, true, '15', '0');
});
it(printTitle('random address', 'cannot slash a node operator by sending 4 ETH and distribute after 14 days'), async () => {
// Process withdraw
await withdrawAndCheck(minipool, '28', nodeWithdrawalAddress, true, '16', '12');
// Wait 14 days and mine enough blocks to pass cooldown
await increaseTime(web3, 60 * 60 * 24 * 14 + 1)
await mineBlocks(web3, 101)
// Process withdraw and attempt to slash
await withdrawAndCheck(minipool, '8', random, false, '8', '0', true);
await shouldRevert(minipool.slash(), 'Was able to slash minipool', 'No balance to slash')
});
it(printTitle('node operator withdrawal address', 'can process withdrawal when balance is less than 16 ETH'), async () => {
// Process withdraw
await withdrawAndCheck(minipool, '15', nodeWithdrawalAddress, false, '15', '0');
});
it(printTitle('node operator withdrawal address', 'should fail when trying to distribute rewards with greater than 8 ETH balance'), async () => {
// Process withdraw
await web3.eth.sendTransaction({
from: random,
to: minipool.address,
gas: 12450000,
value: '8.001'.ether
});
await shouldRevert(minipool.distributeBalance(true, {
from: nodeWithdrawalAddress
}), 'Distribute succeeded', 'Balance exceeds 8 ether');
});
// ETH penalty events
it(printTitle('node operator withdrawal address', 'can process withdrawal and finalise pool when penalised by DAO'), async () => {
// Penalise the minipool 50% of it's ETH
await penaltyTestContract.setPenaltyRate(minipool.address, maxPenaltyRate);
// Process withdraw - 36 ETH would normally give node operator 19 and user 17 but with a 50% penalty, and extra 9.5 goes to the user
await withdrawAndCheck(minipool, '36', nodeWithdrawalAddress, true, '26.5', '9.5');
});
it(printTitle('node operator withdrawal address', 'cannot be penalised greater than the max penalty rate set by DAO'), async () => {
// Try to penalise the minipool 75% of it's ETH (max is 50%)
await penaltyTestContract.setPenaltyRate(minipool.address, web3.utils.toWei('0.75'));
// Process withdraw - 36 ETH would normally give node operator 19 and user 17 but with a 50% penalty, and extra 9.5 goes to the user
await withdrawAndCheck(minipool, '36', nodeWithdrawalAddress, true, '26.5', '9.5');
});
it(printTitle('guardian', 'can disable penalising all together'), async () => {
// Disable penalising by setting rate to 0
const rocketMinipoolPenalty = await RocketMinipoolPenalty.deployed();
await rocketMinipoolPenalty.setMaxPenaltyRate('0', {from: owner})
// Try to penalise the minipool 50%
await penaltyTestContract.setPenaltyRate(minipool.address, web3.utils.toWei('0.5'));
// Process withdraw
await withdrawAndCheck(minipool, '36', nodeWithdrawalAddress, true, '17', '19');
});
})
}