@nodeset/contracts
Version:
Protocol for accessing NodeSet's Constellation Ethereum staking network
445 lines (368 loc) • 27.2 kB
text/typescript
import { expect } from "chai";
import { ethers, upgrades, hardhatArguments } from "hardhat";
import { loadFixture } from "@nomicfoundation/hardhat-network-helpers";
import { Protocol, protocolFixture, RocketPool, SetupData, Signers } from "../test";
import { prepareOperatorDistributionContract, registerNewValidator } from "../utils/utils";
import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers";
import { RocketMinipoolDelegate } from "../../typechain-types";
import { BigNumber } from "ethers";
describe("Exiting Minipools", function () {
let protocol: Protocol;
let signers: Signers;
let rocketPool: RocketPool;
let caller: SignerWithAddress;
let setupData: SetupData
let minipool: RocketMinipoolDelegate;
let subNodeOperator: SignerWithAddress;
let nodeRefundBalance: BigNumber;
let nodeDepositBalance: BigNumber;
beforeEach(async () => {
setupData = await loadFixture(protocolFixture);
protocol = setupData.protocol;
signers = setupData.signers;
rocketPool = setupData.rocketPool;
subNodeOperator = signers.random;
const initialDeposit = await prepareOperatorDistributionContract(setupData, 1);
const minipools = await registerNewValidator(setupData, [subNodeOperator]);
minipool = await ethers.getContractAt("RocketMinipoolDelegate", minipools[0]);
nodeRefundBalance = await minipool.getNodeRefundBalance();
nodeDepositBalance = await minipool.getNodeDepositBalance();
expect(await minipool.getStatus()).equals(2);
})
describe("When Node Refund Balance is zero", async () => {
describe("When caller is admin", async () => {
beforeEach(async () => {
caller = signers.admin;
expect(nodeRefundBalance).equals(0)
})
describe("When received amount is greater than original bond", async () => {
it("rewards should be positive", async () => {
// simulate an exit with rewards
const baconReward = ethers.utils.parseEther("1")
await signers.ethWhale.sendTransaction({
to: minipool.address,
value: baconReward.add(ethers.utils.parseEther("32"))
})
expect(await ethers.provider.getBalance(minipool.address)).equals(ethers.utils.parseEther("33"));
// all of node refund balance is rewards in this scenario
const constellationPortion = baconReward.mul(ethers.utils.parseEther(".3625")).div(ethers.utils.parseEther("1"));
const xrETHPortion = await protocol.vCWETH.getIncomeAfterFees(constellationPortion);
const priorAssets = await protocol.vCWETH.totalAssets();
expect(await protocol.whitelist.getActiveValidatorCountForOperator(subNodeOperator.address)).equals(1)
expect(await protocol.superNode.getNumMinipools()).equals(1);
expect((await protocol.superNode.minipoolData(minipool.address)).subNodeOperator).equals(subNodeOperator.address);
expect(await protocol.operatorDistributor.oracleError()).equals(0)
await protocol.operatorDistributor.connect(caller).distributeExitedMinipool(minipool.address);
expect(await protocol.whitelist.getActiveValidatorCountForOperator(subNodeOperator.address)).equals(0)
expect(await ethers.provider.getBalance(minipool.address)).equals(0);
expect(await protocol.superNode.getNumMinipools()).equals(0);
expect((await protocol.superNode.minipoolData(minipool.address)).subNodeOperator).equals(ethers.constants.AddressZero);
expect(await protocol.vCWETH.totalAssets()).to.equal(priorAssets.add(xrETHPortion));
expect(await protocol.operatorDistributor.oracleError()).greaterThan(0)
})
})
describe("When received amount is equal to original bond", async () => {
it("Should receive no rewards", async () => {
// simulate an exit
await signers.ethWhale.sendTransaction({
to: minipool.address,
value: ethers.utils.parseEther("32")
})
expect(await ethers.provider.getBalance(minipool.address)).equals(ethers.utils.parseEther("32"));
// all of node refund balance is rewards in this scenario
const priorAssets = await protocol.vCWETH.totalAssets();
expect(await protocol.whitelist.getActiveValidatorCountForOperator(subNodeOperator.address)).equals(1)
expect(await protocol.superNode.getNumMinipools()).equals(1);
expect((await protocol.superNode.minipoolData(minipool.address)).subNodeOperator).equals(subNodeOperator.address);
expect(await protocol.operatorDistributor.oracleError()).equals(0)
await protocol.operatorDistributor.connect(caller).distributeExitedMinipool(minipool.address);
expect(await protocol.whitelist.getActiveValidatorCountForOperator(subNodeOperator.address)).equals(0)
expect(await ethers.provider.getBalance(minipool.address)).equals(0);
expect(await protocol.superNode.getNumMinipools()).equals(0);
expect((await protocol.superNode.minipoolData(minipool.address)).subNodeOperator).equals(ethers.constants.AddressZero);
expect(await protocol.vCWETH.totalAssets()).to.equal(priorAssets);
expect(await protocol.operatorDistributor.oracleError()).equals(0)
})
})
describe("When received amount is less than original bond", async () => {
it("Should receive no rewards", async () => {
// simulate an exit
await signers.ethWhale.sendTransaction({
to: minipool.address,
value: ethers.utils.parseEther("31")
})
expect(await ethers.provider.getBalance(minipool.address)).equals(ethers.utils.parseEther("31"));
// all of node refund balance is rewards in this scenario
const priorAssets = await protocol.vCWETH.totalAssets();
expect(await protocol.whitelist.getActiveValidatorCountForOperator(subNodeOperator.address)).equals(1)
expect(await protocol.superNode.getNumMinipools()).equals(1);
expect((await protocol.superNode.minipoolData(minipool.address)).subNodeOperator).equals(subNodeOperator.address);
expect(await protocol.operatorDistributor.oracleError()).equals(0)
await protocol.operatorDistributor.connect(caller).distributeExitedMinipool(minipool.address);
expect(await protocol.whitelist.getActiveValidatorCountForOperator(subNodeOperator.address)).equals(0)
expect(await ethers.provider.getBalance(minipool.address)).equals(0);
expect(await protocol.superNode.getNumMinipools()).equals(0);
expect((await protocol.superNode.minipoolData(minipool.address)).subNodeOperator).equals(ethers.constants.AddressZero);
expect(await protocol.vCWETH.totalAssets()).to.equal(priorAssets.sub(ethers.utils.parseEther("1")));
expect(await protocol.operatorDistributor.oracleError()).equals(0)
})
})
})
describe("When caller is protocol", async () => {
beforeEach(async () => {
caller = signers.protocolSigner;
})
describe("When received amount is greater than original bond", async () => {
it("rewards should be positive", async () => {
// simulate an exit with rewards
const baconReward = ethers.utils.parseEther("1")
await signers.ethWhale.sendTransaction({
to: minipool.address,
value: baconReward.add(ethers.utils.parseEther("32"))
})
expect(await ethers.provider.getBalance(minipool.address)).equals(ethers.utils.parseEther("33"));
// all of node refund balance is rewards in this scenario
const constellationPortion = baconReward.mul(ethers.utils.parseEther(".3625")).div(ethers.utils.parseEther("1"));
const xrETHPortion = await protocol.vCWETH.getIncomeAfterFees(constellationPortion);
const priorAssets = await protocol.vCWETH.totalAssets();
expect(await protocol.whitelist.getActiveValidatorCountForOperator(subNodeOperator.address)).equals(1)
expect(await protocol.superNode.getNumMinipools()).equals(1);
expect((await protocol.superNode.minipoolData(minipool.address)).subNodeOperator).equals(subNodeOperator.address);
expect(await protocol.operatorDistributor.oracleError()).equals(0)
await protocol.operatorDistributor.connect(caller).distributeExitedMinipool(minipool.address);
expect(await protocol.whitelist.getActiveValidatorCountForOperator(subNodeOperator.address)).equals(0)
expect(await ethers.provider.getBalance(minipool.address)).equals(0);
expect(await protocol.superNode.getNumMinipools()).equals(0);
expect((await protocol.superNode.minipoolData(minipool.address)).subNodeOperator).equals(ethers.constants.AddressZero);
expect(await protocol.vCWETH.totalAssets()).to.equal(priorAssets.add(xrETHPortion));
expect(await protocol.operatorDistributor.oracleError()).greaterThan(0)
})
})
describe("When received amount is equal to original bond", async () => {
it("Should receive no rewards", async () => {
// simulate an exit
await signers.ethWhale.sendTransaction({
to: minipool.address,
value: ethers.utils.parseEther("32")
})
expect(await ethers.provider.getBalance(minipool.address)).equals(ethers.utils.parseEther("32"));
// all of node refund balance is rewards in this scenario
const priorAssets = await protocol.vCWETH.totalAssets();
expect(await protocol.whitelist.getActiveValidatorCountForOperator(subNodeOperator.address)).equals(1)
expect(await protocol.superNode.getNumMinipools()).equals(1);
expect((await protocol.superNode.minipoolData(minipool.address)).subNodeOperator).equals(subNodeOperator.address);
expect(await protocol.operatorDistributor.oracleError()).equals(0)
await protocol.operatorDistributor.connect(caller).distributeExitedMinipool(minipool.address);
expect(await protocol.whitelist.getActiveValidatorCountForOperator(subNodeOperator.address)).equals(0)
expect(await ethers.provider.getBalance(minipool.address)).equals(0);
expect(await protocol.superNode.getNumMinipools()).equals(0);
expect((await protocol.superNode.minipoolData(minipool.address)).subNodeOperator).equals(ethers.constants.AddressZero);
expect(await protocol.vCWETH.totalAssets()).to.equal(priorAssets);
expect(await protocol.operatorDistributor.oracleError()).equals(0)
})
})
describe("When received amount is less than original bond", async () => {
it("Should receive no rewards", async () => {
// simulate an exit
await signers.ethWhale.sendTransaction({
to: minipool.address,
value: ethers.utils.parseEther("31")
})
expect(await ethers.provider.getBalance(minipool.address)).equals(ethers.utils.parseEther("31"));
// all of node refund balance is rewards in this scenario
const priorAssets = await protocol.vCWETH.totalAssets();
expect(await protocol.whitelist.getActiveValidatorCountForOperator(subNodeOperator.address)).equals(1)
expect(await protocol.superNode.getNumMinipools()).equals(1);
expect((await protocol.superNode.minipoolData(minipool.address)).subNodeOperator).equals(subNodeOperator.address);
expect(await protocol.operatorDistributor.oracleError()).equals(0)
await protocol.operatorDistributor.connect(caller).distributeExitedMinipool(minipool.address);
expect(await protocol.whitelist.getActiveValidatorCountForOperator(subNodeOperator.address)).equals(0)
expect(await ethers.provider.getBalance(minipool.address)).equals(0);
expect(await protocol.superNode.getNumMinipools()).equals(0);
expect((await protocol.superNode.minipoolData(minipool.address)).subNodeOperator).equals(ethers.constants.AddressZero);
expect(await protocol.vCWETH.totalAssets()).to.equal(priorAssets.sub(ethers.utils.parseEther("1")));
expect(await protocol.operatorDistributor.oracleError()).equals(0)
})
})
})
})
describe("When Node Refund Balance is 1 eth", async () => {
let newNodeRefundAmount: BigNumber;
beforeEach(async () => {
newNodeRefundAmount = ethers.utils.parseEther("1");
const value = ethers.utils.hexZeroPad(ethers.utils.hexlify(newNodeRefundAmount), 32);
const slot = await minipool.getNodeRefundBalanceSlot();
await ethers.provider.send("hardhat_setStorageAt", [
'0x' + minipool.address,
'0x' + ethers.utils.stripZeros(slot),
value
]);
nodeRefundBalance = await minipool.getNodeRefundBalance();
nodeDepositBalance = await minipool.getNodeDepositBalance();
await signers.ethWhale.sendTransaction({
to: minipool.address,
value: newNodeRefundAmount
})
expect(nodeRefundBalance).equals(newNodeRefundAmount);
expect(await ethers.provider.getBalance(minipool.address)).equals(nodeRefundBalance);
})
describe("When caller is admin", async () => {
beforeEach(async () => {
caller = signers.admin;
})
describe("When received amount is greater than original bond", async () => {
it("rewards should be positive", async () => {
// simulate an exit
await signers.ethWhale.sendTransaction({
to: minipool.address,
value: ethers.utils.parseEther("32") // now 33 eth total balance
})
expect(await ethers.provider.getBalance(minipool.address)).equals(ethers.utils.parseEther("33"));
// all of node refund balance is rewards in this scenario
const xrETHPortion = await protocol.vCWETH.getIncomeAfterFees(nodeRefundBalance);
const priorAssets = await protocol.vCWETH.totalAssets();
expect(await protocol.whitelist.getActiveValidatorCountForOperator(subNodeOperator.address)).equals(1)
expect(await protocol.superNode.getNumMinipools()).equals(1);
expect((await protocol.superNode.minipoolData(minipool.address)).subNodeOperator).equals(subNodeOperator.address);
expect(await protocol.operatorDistributor.oracleError()).equals(0)
await protocol.operatorDistributor.connect(caller).distributeExitedMinipool(minipool.address);
expect(await protocol.whitelist.getActiveValidatorCountForOperator(subNodeOperator.address)).equals(0)
expect(await ethers.provider.getBalance(minipool.address)).equals(0);
expect(await protocol.superNode.getNumMinipools()).equals(0);
expect((await protocol.superNode.minipoolData(minipool.address)).subNodeOperator).equals(ethers.constants.AddressZero);
expect(await protocol.vCWETH.totalAssets()).to.equal(priorAssets.add(xrETHPortion));
expect(await protocol.operatorDistributor.oracleError()).greaterThan(0)
})
})
describe("When received amount is equal to original bond", async () => {
it("Should receive no rewards", async () => {
// simulate an exit
await signers.ethWhale.sendTransaction({
to: minipool.address,
value: ethers.utils.parseEther("31") // now 32 eth total balance
})
expect(await ethers.provider.getBalance(minipool.address)).equals(ethers.utils.parseEther("32"));
// all of node refund balance is rewards in this scenario
const priorAssets = await protocol.vCWETH.totalAssets();
expect(await protocol.whitelist.getActiveValidatorCountForOperator(subNodeOperator.address)).equals(1)
expect(await protocol.superNode.getNumMinipools()).equals(1);
expect((await protocol.superNode.minipoolData(minipool.address)).subNodeOperator).equals(subNodeOperator.address);
expect(await protocol.operatorDistributor.oracleError()).equals(0)
await protocol.operatorDistributor.connect(caller).distributeExitedMinipool(minipool.address);
expect(await protocol.whitelist.getActiveValidatorCountForOperator(subNodeOperator.address)).equals(0)
expect(await ethers.provider.getBalance(minipool.address)).equals(0);
expect(await protocol.superNode.getNumMinipools()).equals(0);
expect((await protocol.superNode.minipoolData(minipool.address)).subNodeOperator).equals(ethers.constants.AddressZero);
expect(await protocol.vCWETH.totalAssets()).to.equal(priorAssets);
expect(await protocol.operatorDistributor.oracleError()).equals(0)
})
})
describe("When received amount is less than original bond", async () => {
it("Should receive no rewards", async () => {
// simulate an exit
await signers.ethWhale.sendTransaction({
to: minipool.address,
value: ethers.utils.parseEther("30") // now 31 eth total balance
})
expect(await ethers.provider.getBalance(minipool.address)).equals(ethers.utils.parseEther("31"));
// all of node refund balance is rewards in this scenario
const priorAssets = await protocol.vCWETH.totalAssets();
expect(await protocol.whitelist.getActiveValidatorCountForOperator(subNodeOperator.address)).equals(1)
expect(await protocol.superNode.getNumMinipools()).equals(1);
expect((await protocol.superNode.minipoolData(minipool.address)).subNodeOperator).equals(subNodeOperator.address);
expect(await protocol.operatorDistributor.oracleError()).equals(0)
await protocol.operatorDistributor.connect(caller).distributeExitedMinipool(minipool.address);
expect(await protocol.whitelist.getActiveValidatorCountForOperator(subNodeOperator.address)).equals(0)
expect(await ethers.provider.getBalance(minipool.address)).equals(0);
expect(await protocol.superNode.getNumMinipools()).equals(0);
expect((await protocol.superNode.minipoolData(minipool.address)).subNodeOperator).equals(ethers.constants.AddressZero);
expect(await protocol.vCWETH.totalAssets()).to.equal(priorAssets.sub(ethers.utils.parseEther("1")));
expect(await protocol.operatorDistributor.oracleError()).equals(0)
})
})
})
describe("When caller is protocol", async () => {
beforeEach(async () => {
caller = signers.protocolSigner;
})
describe("When received amount is greater than original bond", async () => {
it("rewards should be positive", async () => {
// simulate an exit
await signers.ethWhale.sendTransaction({
to: minipool.address,
value: ethers.utils.parseEther("32") // now 33 eth total balance
})
expect(await ethers.provider.getBalance(minipool.address)).equals(ethers.utils.parseEther("33"));
// all of node refund balance is rewards in this scenario
const xrETHPortion = await protocol.vCWETH.getIncomeAfterFees(nodeRefundBalance);
const priorAssets = await protocol.vCWETH.totalAssets();
expect(await protocol.whitelist.getActiveValidatorCountForOperator(subNodeOperator.address)).equals(1)
expect(await protocol.superNode.getNumMinipools()).equals(1);
expect((await protocol.superNode.minipoolData(minipool.address)).subNodeOperator).equals(subNodeOperator.address);
expect(await protocol.operatorDistributor.oracleError()).equals(0)
await protocol.operatorDistributor.connect(caller).distributeExitedMinipool(minipool.address);
expect(await protocol.whitelist.getActiveValidatorCountForOperator(subNodeOperator.address)).equals(0)
expect(await ethers.provider.getBalance(minipool.address)).equals(0);
expect(await protocol.superNode.getNumMinipools()).equals(0);
expect((await protocol.superNode.minipoolData(minipool.address)).subNodeOperator).equals(ethers.constants.AddressZero);
expect(await protocol.vCWETH.totalAssets()).to.equal(priorAssets.add(xrETHPortion));
expect(await protocol.operatorDistributor.oracleError()).greaterThan(0)
})
})
describe("When received amount is equal to original bond", async () => {
it("Should receive no rewards", async () => {
// simulate an exit
await signers.ethWhale.sendTransaction({
to: minipool.address,
value: ethers.utils.parseEther("31") // now 32 eth total balance
})
expect(await ethers.provider.getBalance(minipool.address)).equals(ethers.utils.parseEther("32"));
// all of node refund balance is rewards in this scenario
const priorAssets = await protocol.vCWETH.totalAssets();
expect(await protocol.whitelist.getActiveValidatorCountForOperator(subNodeOperator.address)).equals(1)
expect(await protocol.superNode.getNumMinipools()).equals(1);
expect((await protocol.superNode.minipoolData(minipool.address)).subNodeOperator).equals(subNodeOperator.address);
expect(await protocol.operatorDistributor.oracleError()).equals(0)
await protocol.operatorDistributor.connect(caller).distributeExitedMinipool(minipool.address);
expect(await protocol.whitelist.getActiveValidatorCountForOperator(subNodeOperator.address)).equals(0)
expect(await ethers.provider.getBalance(minipool.address)).equals(0);
expect(await protocol.superNode.getNumMinipools()).equals(0);
expect((await protocol.superNode.minipoolData(minipool.address)).subNodeOperator).equals(ethers.constants.AddressZero);
expect(await protocol.vCWETH.totalAssets()).to.equal(priorAssets);
expect(await protocol.operatorDistributor.oracleError()).equals(0)
})
})
describe("When received amount is less than original bond", async () => {
it("Should receive no rewards", async () => {
// simulate an exit
await signers.ethWhale.sendTransaction({
to: minipool.address,
value: ethers.utils.parseEther("30") // now 31 eth total balance
})
expect(await ethers.provider.getBalance(minipool.address)).equals(ethers.utils.parseEther("31"));
// all of node refund balance is rewards in this scenario
const priorAssets = await protocol.vCWETH.totalAssets();
expect(await protocol.whitelist.getActiveValidatorCountForOperator(subNodeOperator.address)).equals(1)
expect(await protocol.superNode.getNumMinipools()).equals(1);
expect((await protocol.superNode.minipoolData(minipool.address)).subNodeOperator).equals(subNodeOperator.address);
expect(await protocol.operatorDistributor.oracleError()).equals(0)
await protocol.operatorDistributor.connect(caller).distributeExitedMinipool(minipool.address);
expect(await protocol.whitelist.getActiveValidatorCountForOperator(subNodeOperator.address)).equals(0)
expect(await ethers.provider.getBalance(minipool.address)).equals(0);
expect(await protocol.superNode.getNumMinipools()).equals(0);
expect((await protocol.superNode.minipoolData(minipool.address)).subNodeOperator).equals(ethers.constants.AddressZero);
expect(await protocol.vCWETH.totalAssets()).to.equal(priorAssets.sub(ethers.utils.parseEther("1")));
expect(await protocol.operatorDistributor.oracleError()).equals(0)
})
})
})
})
describe("When caller is neither admin nor protocol", async () => {
beforeEach(async () => {
caller = signers.random;
})
it("Should revert", async () => {
await expect(protocol.operatorDistributor.connect(caller).distributeExitedMinipool(minipool.address)).to.be.revertedWith("Can only be called by Protocol or Admin!")
})
})
})