@nodeset/contracts
Version:
Protocol for accessing NodeSet's Constellation Ethereum staking network
401 lines (312 loc) • 24.3 kB
text/typescript
import { expect } from "chai";
import { ethers } from "hardhat";
import { loadFixture } from "@nomicfoundation/hardhat-network-helpers";
import { protocolFixture, SetupData } from "./test";
import { BigNumber } from "ethers";
import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers"
import { expectNumberE18ToBeApproximately, getEventNames, prepareOperatorDistributionContract, registerNewValidator, upgradePriceFetcherToMock, whitelistUserServerSig } from "./utils/utils";
import { ContractTransaction } from "@ethersproject/contracts";
import { wEth } from "../typechain-types/factories/contracts/Testing";
describe("xrETH", function () {
it("success - test initial xrETH values", async () => {
const setupData = await loadFixture(protocolFixture);
const { protocol, signers, rocketPool } = setupData;
const name = await protocol.vCWETH.name()
const symbol = await protocol.vCWETH.symbol();
expect(name).equals("Constellation ETH");
expect(symbol).equals("xrETH");
expect(await protocol.vCWETH.liquidityReservePercent()).equals(ethers.utils.parseUnits("0.1", 18))
expect(await protocol.vCWETH.maxWethRplRatio()).equals(ethers.utils.parseUnits("40", 18))
expect(await protocol.vCWETH.treasuryFee()).equals(ethers.utils.parseUnits("0.14788", 18))
expect(await protocol.vCWETH.nodeOperatorFee()).equals(ethers.utils.parseUnits("0.14788", 18))
})
it("fail - tries to deposit weth as 'bad actor' involved in AML or other flavors of bad", async () => {
const setupData = await loadFixture(protocolFixture);
const { protocol, signers, rocketPool } = setupData;
await protocol.sanctions.addBlacklist(signers.ethWhale.address);
await protocol.directory.connect(signers.admin).enableSanctions();
expect(await protocol.sanctions.isSanctioned(signers.ethWhale.address)).equals(true);
await protocol.wETH.connect(signers.ethWhale).deposit({ value: ethers.utils.parseEther("100") });
await protocol.wETH.connect(signers.ethWhale).approve(protocol.vCWETH.address, ethers.utils.parseEther("100"));
const tx = await protocol.vCWETH.connect(signers.ethWhale).deposit(ethers.utils.parseEther("100"), signers.ethWhale.address);
const receipt = await tx.wait();
const { events } = receipt;
if (events) {
for (let i = 0; i < events.length; i++) {
expect(events[i].event).not.equals(null)
if (events[i].event?.includes("SanctionViolation")) {
expect(events[i].event?.includes("SanctionViolation")).equals(true)
}
}
}
const expectedxrETHInSystem = ethers.utils.parseEther("0");
const actualxrETHInSystem = await protocol.vCWETH.totalAssets();
expect(expectedxrETHInSystem).equals(actualxrETHInSystem)
})
it("success - tries to deposit weth as 'good actor' not involved in AML or other flavors of bad", async () => {
const setupData = await loadFixture(protocolFixture);
const { protocol, signers, rocketPool } = setupData;
const {sig} = await whitelistUserServerSig(setupData, signers.ethWhale);
await rocketPool.rplContract.connect(signers.rplWhale).transfer(signers.ethWhale.address, ethers.utils.parseEther("100"));
await protocol.whitelist.connect(signers.admin).addOperator(signers.ethWhale.address, sig);
await protocol.wETH.connect(signers.ethWhale).deposit({ value: ethers.utils.parseEther("100") });
await protocol.wETH.connect(signers.ethWhale).approve(protocol.vCWETH.address, ethers.utils.parseEther("100"));
await protocol.vCWETH.connect(signers.ethWhale).deposit(ethers.utils.parseEther("100"), signers.ethWhale.address);
const expectedxrETHInSystem = ethers.utils.parseEther("99.97"); // with 0.03% mint fee
const actualxrETHInSystem = await protocol.vCWETH.totalAssets();
expect(expectedxrETHInSystem).equals(actualxrETHInSystem)
})
it("success - tries to deposit once, then redeem from weth vault multiple times", async () => {
const setupData = await loadFixture(protocolFixture);
const { protocol, signers, rocketPool } = setupData;
const depositAmount = ethers.utils.parseEther("100");
const depositAfterFee = depositAmount.sub(await protocol.vCWETH.getMintFeePortion(depositAmount));
expect(depositAfterFee).equals(await protocol.vCWETH.previewDeposit(depositAmount));
const expectedReserveInVault = (await protocol.vCWETH.getMissingLiquidityAfterDeposit(depositAmount));
const surplusSentToOD = depositAfterFee.sub(expectedReserveInVault);
await protocol.wETH.connect(signers.random).deposit({ value: depositAmount });
await protocol.wETH.connect(signers.random).approve(protocol.vCWETH.address, depositAmount);
await protocol.vCWETH.connect(signers.random).deposit(depositAmount, signers.random.address);
expect(await protocol.vCWETH.totalAssets()).equals(depositAfterFee)
expect(await protocol.wETH.balanceOf(protocol.vCWETH.address)).equals(expectedReserveInVault);
expect(await ethers.provider.getBalance(protocol.operatorDistributor.address)).equals(surplusSentToOD);
const shareValue = await protocol.vCWETH.convertToAssets(ethers.utils.parseEther("1"))
const expectedRedeemValue = await protocol.vCWETH.previewRedeem(shareValue);
let preBalance = await protocol.wETH.balanceOf(signers.random.address);
await protocol.vCWETH.connect(signers.random).redeem(shareValue, signers.random.address, signers.random.address);
let postBalance = await protocol.wETH.balanceOf(signers.random.address);
expect(expectedRedeemValue).equals(postBalance.sub(preBalance));
let expectedTotalAssets = depositAfterFee.sub(expectedRedeemValue);
expect(await protocol.vCWETH.totalAssets()).equals(expectedTotalAssets);
expect(await protocol.wETH.balanceOf(protocol.vCWETH.address)).equals(
((await protocol.vCWETH.totalAssets())
.mul(await protocol.vCWETH.liquidityReservePercent())
.div(ethers.utils.parseEther("1"))
));
preBalance = await protocol.wETH.balanceOf(signers.random.address);
await protocol.vCWETH.connect(signers.random).redeem(shareValue, signers.random.address, signers.random.address);
postBalance = await protocol.wETH.balanceOf(signers.random.address);
expect(expectedRedeemValue).equals(postBalance.sub(preBalance));
expectedTotalAssets = expectedTotalAssets.sub(expectedRedeemValue);
expect(await protocol.vCWETH.totalAssets()).equals(expectedTotalAssets);
expect(await protocol.wETH.balanceOf(protocol.vCWETH.address)).equals(
(await protocol.vCWETH.totalAssets())
.mul(await protocol.vCWETH.liquidityReservePercent())
.div(ethers.utils.parseEther("1"))
);
preBalance = await protocol.wETH.balanceOf(signers.random.address);
await protocol.vCWETH.connect(signers.random).redeem(shareValue, signers.random.address, signers.random.address);
postBalance = await protocol.wETH.balanceOf(signers.random.address);
expect(expectedRedeemValue).equals(postBalance.sub(preBalance));
expectedTotalAssets = expectedTotalAssets.sub(expectedRedeemValue);
expect(await protocol.vCWETH.totalAssets()).equals(expectedTotalAssets);
expect(await protocol.wETH.balanceOf(protocol.vCWETH.address)).equals(
(await protocol.vCWETH.totalAssets())
.mul(await protocol.vCWETH.liquidityReservePercent())
.div(ethers.utils.parseEther("1"))
);
})
it("success - tries to deposit and redeem from weth vault multiple times with minipool reward claims", async () => {
const setupData = await loadFixture(protocolFixture);
const { protocol, signers, rocketPool } = setupData;
const initialDeposit = await prepareOperatorDistributionContract(setupData, 1);
const minipools = await registerNewValidator(setupData, [signers.random]);
const depositAmount = ethers.utils.parseEther("100");
const totalDeposit = initialDeposit.add(depositAmount);
const depositAfterFee = totalDeposit.sub(await protocol.vCWETH.getMintFeePortion(totalDeposit));
expect(depositAfterFee).equals(await protocol.vCWETH.previewDeposit(totalDeposit));
await protocol.wETH.connect(signers.random).deposit({ value: depositAmount });
await protocol.wETH.connect(signers.random).approve(protocol.vCWETH.address, depositAmount);
await protocol.vCWETH.connect(signers.random).deposit(depositAmount, signers.random.address);
expect(await protocol.vCWETH.totalAssets()).equals(totalDeposit.sub(await protocol.vCWETH.getMintFeePortion(totalDeposit)))
const shareValue = await protocol.vCWETH.convertToAssets(ethers.utils.parseEther("1"))
const initialRedeemValue = await protocol.vCWETH.previewRedeem(shareValue);
expect(initialRedeemValue).equals(ethers.utils.parseEther("1"));
// simulate 1 ether of rewards put into minipool contract
const executionLayerReward = ethers.utils.parseEther("1");
await signers.ethWhale.sendTransaction({
to: minipools[0],
value: executionLayerReward
})
console.log('minipool balance', await ethers.provider.getBalance(minipools[0]));
const minipoolData = await protocol.superNode.minipoolData(minipools[0]);
// assume a 15% rETH fee and LEB8 (36.25% of all rewards), which is default settings
const nodeRewards = executionLayerReward.mul(ethers.utils.parseEther(".3625")).div(ethers.utils.parseEther("1"));
const expectedTreasuryPortion = nodeRewards.mul(minipoolData.ethTreasuryFee).div(ethers.utils.parseEther("1"));
const expectedNodeOperatorPortion = nodeRewards.mul(minipoolData.noFee).div(ethers.utils.parseEther("1"));
const expectedCommunityPortion = nodeRewards.sub(expectedTreasuryPortion).sub(expectedNodeOperatorPortion);
const initalTreasuryBalance = await ethers.provider.getBalance(await protocol.directory.getTreasuryAddress());
await protocol.operatorDistributor.connect(signers.random).processNextMinipool();
const finalTreasuryBalance = await ethers.provider.getBalance(await protocol.directory.getTreasuryAddress());
const expectedRedeemValue = await protocol.vCWETH.previewRedeem(shareValue);
expect(await protocol.vCWETH.totalAssets()).equals(depositAfterFee.add(expectedCommunityPortion));
let preBalance = await protocol.wETH.balanceOf(signers.random.address);
await protocol.vCWETH.connect(signers.random).redeem(shareValue, signers.random.address, signers.random.address);
let postBalance = await protocol.wETH.balanceOf(signers.random.address);
expect(expectedRedeemValue).equals(postBalance.sub(preBalance));
preBalance = await protocol.wETH.balanceOf(signers.random.address);
await protocol.vCWETH.connect(signers.random).redeem(shareValue, signers.random.address, signers.random.address);
postBalance = await protocol.wETH.balanceOf(signers.random.address);
expectNumberE18ToBeApproximately(expectedRedeemValue, postBalance.sub(preBalance), 0.0000000001)
preBalance = await protocol.wETH.balanceOf(signers.random.address);
await protocol.vCWETH.connect(signers.random).redeem(shareValue, signers.random.address, signers.random.address);
postBalance = await protocol.wETH.balanceOf(signers.random.address);
expectNumberE18ToBeApproximately(expectedRedeemValue, postBalance.sub(preBalance), 0.0000000001)
// preview of redeeming all shares
const fullPreviewRedeem = (await protocol.vCWETH.previewRedeem((await protocol.vCWETH.balanceOf(signers.random.address)).add(initialDeposit).sub(await protocol.vCWETH.getMintFeePortion(initialDeposit))));
expectNumberE18ToBeApproximately(fullPreviewRedeem, (await protocol.vCWETH.totalAssets()), .00000001);
expect(await ethers.provider.getBalance(protocol.yieldDistributor.address)).equals(expectedNodeOperatorPortion);
expect(finalTreasuryBalance.sub(initalTreasuryBalance)).equals(expectedTreasuryPortion);
})
it("success - tries to deposit and redeem from weth vault multiple times with a minipool reward claim, simulating a penalized exit", async () => {
const setupData = await loadFixture(protocolFixture);
const { protocol, signers, rocketPool } = setupData;
expect(await protocol.wETH.balanceOf(protocol.directory.getTreasuryAddress())).equals(0);
const initialDeposit = await prepareOperatorDistributionContract(setupData, 1);
expect(await protocol.wETH.balanceOf(protocol.directory.getTreasuryAddress())).equals(await protocol.vCWETH.getMintFeePortion(initialDeposit));
const minipools = await registerNewValidator(setupData, [signers.random]);
const depositAmount = ethers.utils.parseEther("100");
const totalDeposit = initialDeposit.add(depositAmount);
const totalDepositAfterMintFee = totalDeposit.sub(await protocol.vCWETH.getMintFeePortion(totalDeposit));
await protocol.wETH.connect(signers.random).deposit({ value: depositAmount });
await protocol.wETH.connect(signers.random).approve(protocol.vCWETH.address, depositAmount);
await protocol.vCWETH.connect(signers.random).deposit(depositAmount, signers.random.address);
expect(await protocol.wETH.balanceOf(protocol.directory.getTreasuryAddress())).equals(await protocol.vCWETH.getMintFeePortion(totalDeposit));
expect(await protocol.vCWETH.totalAssets()).equals(totalDepositAfterMintFee)
const shareValue = await protocol.vCWETH.convertToAssets(ethers.utils.parseEther("1"))
const initialRedeemValue = await protocol.vCWETH.previewRedeem(shareValue);
expect(initialRedeemValue).equals(ethers.utils.parseEther("1"));
// simulate 31 ether (whole bond - 1 ETH penalty) put into minipool contract from beacon
const penalty = ethers.utils.parseEther("1");
const finalMinipoolBalance = ethers.utils.parseEther("32").sub(penalty);
await signers.ethWhale.sendTransaction({
to: minipools[0],
value: finalMinipoolBalance
})
await protocol.operatorDistributor.connect(signers.random).processNextMinipool();
expect(await protocol.wETH.balanceOf(protocol.directory.getTreasuryAddress())).equals(await protocol.vCWETH.getMintFeePortion(totalDeposit));
await protocol.operatorDistributor.connect(signers.admin).distributeExitedMinipool(minipools[0])
expect(await protocol.wETH.balanceOf(protocol.directory.getTreasuryAddress())).equals(await protocol.vCWETH.getMintFeePortion(totalDeposit));
const expectedRedeemValue = await protocol.vCWETH.previewRedeem(shareValue);
expect(await protocol.vCWETH.totalAssets()).equals(totalDepositAfterMintFee.sub(penalty));
let preBalance = await protocol.wETH.balanceOf(signers.random.address);
await protocol.vCWETH.connect(signers.random).redeem(shareValue, signers.random.address, signers.random.address);
let postBalance = await protocol.wETH.balanceOf(signers.random.address);
expect(expectedRedeemValue).equals(postBalance.sub(preBalance));
preBalance = await protocol.wETH.balanceOf(signers.random.address);
await protocol.vCWETH.connect(signers.random).redeem(shareValue, signers.random.address, signers.random.address);
postBalance = await protocol.wETH.balanceOf(signers.random.address);
expectNumberE18ToBeApproximately(expectedRedeemValue, postBalance.sub(preBalance), 0.0000000001)
preBalance = await protocol.wETH.balanceOf(signers.random.address);
await protocol.vCWETH.connect(signers.random).redeem(shareValue, signers.random.address, signers.random.address);
postBalance = await protocol.wETH.balanceOf(signers.random.address);
expectNumberE18ToBeApproximately(expectedRedeemValue, postBalance.sub(preBalance), 0.0000000001)
// preview of redeeming all shares
const fullPreviewRedeem = (await protocol.vCWETH.previewRedeem((await protocol.vCWETH.balanceOf(signers.random.address)).add(initialDeposit).sub(await protocol.vCWETH.getMintFeePortion(initialDeposit))));
expectNumberE18ToBeApproximately(fullPreviewRedeem, (await protocol.vCWETH.totalAssets()), .00000001);
expect(await ethers.provider.getBalance(protocol.yieldDistributor.address)).equals(0);
expect(await protocol.wETH.balanceOf(protocol.directory.getTreasuryAddress())).equals(await protocol.vCWETH.getMintFeePortion(totalDeposit));
})
it("success - tries to deposit and redeem from weth vault multiple times with a minipool reward claim, simulating an exit with rewards", async () => {
const setupData = await loadFixture(protocolFixture);
const { protocol, signers, rocketPool } = setupData;
const initialDeposit = await prepareOperatorDistributionContract(setupData, 1);
expect(await protocol.wETH.balanceOf(protocol.directory.getTreasuryAddress())).equals(await protocol.vCWETH.getMintFeePortion(initialDeposit));
const minipools = await registerNewValidator(setupData, [signers.random]);
const depositAmount = ethers.utils.parseEther("100");
const totalDeposit = initialDeposit.add(depositAmount);
const totalDepositAfterMintFee = totalDeposit.sub(await protocol.vCWETH.getMintFeePortion(totalDeposit));
await protocol.wETH.connect(signers.random).deposit({ value: depositAmount });
await protocol.wETH.connect(signers.random).approve(protocol.vCWETH.address, depositAmount);
await protocol.vCWETH.connect(signers.random).deposit(depositAmount, signers.random.address);
expect(await protocol.wETH.balanceOf(protocol.directory.getTreasuryAddress())).equals(await protocol.vCWETH.getMintFeePortion(totalDeposit))
expect(await protocol.vCWETH.totalAssets()).equals(totalDepositAfterMintFee);
const shareValue = await protocol.vCWETH.convertToAssets(ethers.utils.parseEther("1"))
const initialRedeemValue = await protocol.vCWETH.previewRedeem(shareValue);
expect(initialRedeemValue).equals(ethers.utils.parseEther("1"));
// simulate 33 ether (full validator + 1 ETH reward) put into minipool contract from beacon
const rewards = ethers.utils.parseEther("1");
const finalMinipoolBalance = ethers.utils.parseEther("32").add(rewards);
await signers.ethWhale.sendTransaction({
to: minipools[0],
value: finalMinipoolBalance
})
const minipoolData = await protocol.superNode.minipoolData(minipools[0]);
// assume a 15% rETH fee and LEB8 (36.25% of all rewards), which is default settings
const nodeRewards = rewards.mul(ethers.utils.parseEther(".3625")).div(ethers.utils.parseEther("1"));
const expectedTreasuryPortion = nodeRewards.mul(minipoolData.ethTreasuryFee).div(ethers.utils.parseEther("1"));
const expectedNodeOperatorPortion = nodeRewards.mul(minipoolData.noFee).div(ethers.utils.parseEther("1"));
const expectedCommunityPortion = nodeRewards.sub(expectedTreasuryPortion).sub(expectedNodeOperatorPortion);
//expect(await protocol.superNode.minipoolIndex(minipools[0])).to.not.equal(0);
expect((await protocol.superNode.minipoolData(minipools[0])).subNodeOperator).to.not.equal(0);
await protocol.operatorDistributor.connect(signers.random).processNextMinipool();
const treasuryFee = (await protocol.vCWETH.getMintFeePortion(totalDeposit));
expect(await protocol.wETH.balanceOf(protocol.directory.getTreasuryAddress())).equals(await protocol.vCWETH.getMintFeePortion(totalDeposit))
expect(await ethers.provider.getBalance(protocol.directory.getTreasuryAddress())).equals(expectedTreasuryPortion)
// expect the minipool to be removed from the SNA accounting
expect((await protocol.superNode.minipoolData(minipools[0])).subNodeOperator).to.equal(ethers.constants.AddressZero);
const expectedRedeemValue = await protocol.vCWETH.previewRedeem(shareValue);
expect(await protocol.vCWETH.totalAssets()).equals(totalDepositAfterMintFee.add(expectedCommunityPortion));
let preBalance = await protocol.wETH.balanceOf(signers.random.address);
await protocol.vCWETH.connect(signers.random).redeem(shareValue, signers.random.address, signers.random.address);
let postBalance = await protocol.wETH.balanceOf(signers.random.address);
expect(expectedRedeemValue).equals(postBalance.sub(preBalance));
preBalance = await protocol.wETH.balanceOf(signers.random.address);
await protocol.vCWETH.connect(signers.random).redeem(shareValue, signers.random.address, signers.random.address);
postBalance = await protocol.wETH.balanceOf(signers.random.address);
expectNumberE18ToBeApproximately(expectedRedeemValue, postBalance.sub(preBalance), 0.0000000001)
preBalance = await protocol.wETH.balanceOf(signers.random.address);
await protocol.vCWETH.connect(signers.random).redeem(shareValue, signers.random.address, signers.random.address);
postBalance = await protocol.wETH.balanceOf(signers.random.address);
expectNumberE18ToBeApproximately(expectedRedeemValue, postBalance.sub(preBalance), 0.0000000001)
// preview of redeeming all shares
const fullPreviewRedeem = (await protocol.vCWETH.previewRedeem((await protocol.vCWETH.balanceOf(signers.random.address)).add(initialDeposit).sub(await protocol.vCWETH.getMintFeePortion(initialDeposit))));
expectNumberE18ToBeApproximately(fullPreviewRedeem, (await protocol.vCWETH.totalAssets()), .00000001);
expect(await ethers.provider.getBalance(protocol.yieldDistributor.address)).equals(expectedNodeOperatorPortion);
expect(await protocol.wETH.balanceOf(protocol.directory.getTreasuryAddress())).equals(await protocol.vCWETH.getMintFeePortion(totalDeposit))
expect(await ethers.provider.getBalance(protocol.directory.getTreasuryAddress())).equals(expectedTreasuryPortion)
})
it("fail - cannot deposit 1 eth at 50 rpl and 500 rpl, tvl ratio returns ~15%", async () => {
const setupData = await loadFixture(protocolFixture);
const { protocol, signers, rocketPool } = setupData;
})
it("success - can deposit 5 eth at 50 rpl and 500 rpl, tvl ratio returns ~15%", async () => {
const setupData = await loadFixture(protocolFixture);
const { protocol, signers, rocketPool } = setupData;
});
describe("admin functions", () => {
it("success - admin can set tvlCoverageRatio", async () => {
const { protocol, signers } = await loadFixture(protocolFixture);
const tvlCoverageRatio = ethers.utils.parseEther("0.1542069");
await protocol.vCWETH.connect(signers.admin).setMaxWethRplRatio(tvlCoverageRatio);
const tvlCoverageRatioFromContract = await protocol.vCWETH.maxWethRplRatio();
expect(tvlCoverageRatioFromContract).equals(tvlCoverageRatio);
});
it("fail - non-admin cannot set tvlCoverageRatio", async () => {
const { protocol, signers } = await loadFixture(protocolFixture);
const tvlCoverageRatio = ethers.utils.parseEther("0.1542069");
await expect(protocol.vCWETH.connect(signers.ethWhale).setMaxWethRplRatio(tvlCoverageRatio)).to.be.revertedWith("Can only be called by short timelock!");
});
});
describe("sanctions checks", () => {
it("success - allows deposits from non-sanctioned senders and origins", async () => {
const { protocol, signers } = await loadFixture(protocolFixture);
const depositAmountEth = ethers.utils.parseEther("5");
await protocol.wETH.connect(signers.ethWhale).deposit({ value: depositAmountEth });
await protocol.wETH.connect(signers.ethWhale).approve(protocol.vCWETH.address, depositAmountEth);
const tx = await protocol.vCWETH.connect(signers.ethWhale).deposit(depositAmountEth, signers.ethWhale.address);
const events = await getEventNames(tx, protocol.directory);
expect(events.includes("SanctionViolation")).equals(false);
})
it("fail - should fail siliently with event logging", async () => {
const { protocol, signers } = await loadFixture(protocolFixture);
const depositAmountEth = ethers.utils.parseEther("5");
await protocol.sanctions.addBlacklist(signers.ethWhale.address);
await protocol.wETH.connect(signers.ethWhale).deposit({ value: depositAmountEth });
await protocol.wETH.connect(signers.ethWhale).approve(protocol.vCWETH.address, depositAmountEth);
const tx = await protocol.vCWETH.connect(signers.ethWhale).deposit(depositAmountEth, signers.ethWhale.address);
const events = await getEventNames(tx, protocol.directory);
expect(events.includes("SanctionViolation")).equals(true);
})
})
});