Skip to content

Commit

Permalink
Merge pull request #1149 from NexusMutual/fix/pool-8-staking-pool-bug…
Browse files Browse the repository at this point in the history
…-fix

fix: Fix processExpiration for pool 8
  • Loading branch information
shark0der authored Jun 26, 2024
2 parents 253d135 + 1f9568e commit 2f4745d
Show file tree
Hide file tree
Showing 6 changed files with 264 additions and 16 deletions.
20 changes: 13 additions & 7 deletions contracts/modules/staking/StakingPool.sol
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ contract StakingPool is IStakingPool, Multicall {
// +2% for every 1%, ie +200% for 100%

// 1 nxm = 1e18
uint public constant ONE_NXM = 1 ether;
uint internal constant ONE_NXM = 1 ether;

// internally we store capacity using 2 decimals
// 1 nxm of capacity is stored as 100
Expand Down Expand Up @@ -214,6 +214,18 @@ contract StakingPool is IStakingPool, Multicall {
_firstActiveTrancheId = currentTrancheId;
}

// bug fix
if (poolId == 8) {
uint lowerTimestamp = Math.min(
(_firstActiveBucketId + 1) * BUCKET_DURATION,
(_firstActiveTrancheId + 1) * TRANCHE_DURATION
);

if (lowerTimestamp < lastAccNxmUpdate) {
lastAccNxmUpdate = lowerTimestamp.toUint32();
}
}

// if a force update was not requested
if (!updateUntilCurrentTimestamp) {

Expand All @@ -240,12 +252,6 @@ contract StakingPool is IStakingPool, Multicall {
return;
}

if (_rewardsSharesSupply == 0) {
// nothing to do, just update lastAccNxmUpdate
lastAccNxmUpdate = block.timestamp.toUint32();
return;
}

while (_firstActiveBucketId < currentBucketId || _firstActiveTrancheId < currentTrancheId) {

// what expires first, the bucket or the tranche?
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "nexusmutual",
"version": "2.6.0",
"version": "2.6.2",
"description": "NexusMutual smart contracts",
"repository": {
"type": "git",
Expand Down
11 changes: 6 additions & 5 deletions test/fork/basic-functionality-tests.js
Original file line number Diff line number Diff line change
Expand Up @@ -108,11 +108,12 @@ describe('basic functionality tests', function () {
await evm.impersonate(NXM_WHALE_2);
await evm.impersonate(NXMHOLDER);
await evm.impersonate(NEW_POOL_MANAGER);
await evm.setBalance(NXM_WHALE_1, parseEther('1000'));
await evm.setBalance(NXM_WHALE_2, parseEther('1000'));
await evm.setBalance(NXMHOLDER, parseEther('1000'));
await evm.setBalance(NEW_POOL_MANAGER, parseEther('1000'));
await evm.setBalance(DAI_HOLDER, parseEther('1000'));
await evm.setBalance(NXM_WHALE_1, parseEther('100000'));
await evm.setBalance(NXM_WHALE_2, parseEther('100000'));
await evm.setBalance(NXMHOLDER, parseEther('100000'));
await evm.setBalance(NEW_POOL_MANAGER, parseEther('100000'));
await evm.setBalance(DAI_HOLDER, parseEther('100000'));
await evm.setBalance(DAI_NXM_HOLDER, parseEther('100000'));

this.members = [];
this.members.push(await getSigner(NXM_WHALE_1));
Expand Down
241 changes: 241 additions & 0 deletions test/fork/process-expiration.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,241 @@
const { ethers, network } = require('hardhat');
const { expect } = require('chai');
const { abis, addresses } = require('@nexusmutual/deployments');

const {
Address,
EnzymeAdress,
V2Addresses,
formatInternalContracts,
getSigner,
submitGovernanceProposal,
} = require('./utils');
const { ContractCode, ProposalCategory: PROPOSAL_CATEGORIES } = require('../../lib/constants');
const evm = require('./evm')();

const { parseEther, defaultAbiCoder, toUtf8Bytes } = ethers.utils;

describe('process-expirations', function () {
async function getContractByContractCode(contractName, contractCode) {
this.master = this.master ?? (await ethers.getContractAt('NXMaster', V2Addresses.NXMaster));
const contractAddress = await this.master?.getLatestAddress(toUtf8Bytes(contractCode));
return ethers.getContractAt(contractName, contractAddress);
}

before(async function () {
// Initialize evm helper
await evm.connect(ethers.provider);

// Get or revert snapshot if network is tenderly
if (network.name === 'tenderly') {
const { TENDERLY_SNAPSHOT_ID } = process.env;
if (TENDERLY_SNAPSHOT_ID) {
await evm.revert(TENDERLY_SNAPSHOT_ID);
console.info(`Reverted to snapshot ${TENDERLY_SNAPSHOT_ID}`);
} else {
console.info('Snapshot ID: ', await evm.snapshot());
}
}
const [deployer] = await ethers.getSigners();
await evm.setBalance(deployer.address, parseEther('1000'));
});

it('load contracts', async function () {
this.mcr = await ethers.getContractAt(abis.MCR, addresses.MCR);
this.cover = await ethers.getContractAt(abis.Cover, addresses.Cover);
this.nxm = await ethers.getContractAt(abis.NXMToken, addresses.NXMToken);
this.master = await ethers.getContractAt(abis.NXMaster, addresses.NXMaster);
this.coverNFT = await ethers.getContractAt(abis.CoverNFT, addresses.CoverNFT);
this.pool = await ethers.getContractAt(abis.Pool, addresses.Pool);
this.safeTracker = await ethers.getContractAt(abis.SafeTracker, addresses.SafeTracker);
this.assessment = await ethers.getContractAt(abis.Assessment, addresses.Assessment);
this.productsV1 = await ethers.getContractAt(abis.ProductsV1, addresses.ProductsV1);
this.stakingNFT = await ethers.getContractAt(abis.StakingNFT, addresses.StakingNFT);
this.stakingProducts = await ethers.getContractAt(abis.StakingProducts, addresses.StakingProducts);
this.swapOperator = await ethers.getContractAt(abis.SwapOperator, addresses.SwapOperator);
this.stakingPool = await ethers.getContractAt(abis.StakingPool, V2Addresses.StakingPoolImpl);
this.priceFeedOracle = await ethers.getContractAt(abis.PriceFeedOracle, addresses.PriceFeedOracle);
this.tokenController = await ethers.getContractAt(abis.TokenController, addresses.TokenController);
this.individualClaims = await ethers.getContractAt(abis.IndividualClaims, addresses.IndividualClaims);
this.quotationData = await ethers.getContractAt(abis.LegacyQuotationData, addresses.LegacyQuotationData);
this.newClaimsReward = await ethers.getContractAt(abis.LegacyClaimsReward, addresses.LegacyClaimsReward);
this.proposalCategory = await ethers.getContractAt(abis.ProposalCategory, addresses.ProposalCategory);
this.stakingPoolFactory = await ethers.getContractAt(abis.StakingPoolFactory, addresses.StakingPoolFactory);
this.pooledStaking = await ethers.getContractAt(abis.LegacyPooledStaking, addresses.LegacyPooledStaking);
this.gateway = await ethers.getContractAt(abis.LegacyGateway, addresses.LegacyGateway);
this.yieldTokenIncidents = await ethers.getContractAt(abis.YieldTokenIncidents, addresses.YieldTokenIncidents);
this.ramm = await ethers.getContractAt(abis.Ramm, addresses.Ramm);

this.governance = await getContractByContractCode(abis.Governance, ContractCode.Governance);
this.memberRoles = await getContractByContractCode(abis.MemberRoles, ContractCode.MemberRoles);

// Token Mocks
this.dai = await ethers.getContractAt('ERC20Mock', Address.DAI_ADDRESS);
this.rEth = await ethers.getContractAt('ERC20Mock', Address.RETH_ADDRESS);
this.stEth = await ethers.getContractAt('ERC20Mock', Address.STETH_ADDRESS);
this.usdc = await ethers.getContractAt('ERC20Mock', Address.USDC_ADDRESS);
this.enzymeShares = await ethers.getContractAt('ERC20Mock', EnzymeAdress.ENZYMEV4_VAULT_PROXY_ADDRESS);
});

it('Impersonate AB members', async function () {
const { memberArray: abMembers } = await this.memberRoles.members(1);
this.abMembers = [];
for (const address of abMembers) {
await evm.impersonate(address);
await evm.setBalance(address, parseEther('1000'));
this.abMembers.push(await getSigner(address));
}
});

it('Collect storage data before upgrade', async function () {
const stakingPoolCount = (await this.stakingPoolFactory.stakingPoolCount()).toNumber();
this.contractData = {
cover: { before: {}, after: {} },
};

// Cover
const productTypesCount = await this.cover.productTypesCount();

this.contractData.cover.before.productTypes = [];
this.contractData.cover.before.productTypeNames = [];
this.contractData.cover.before.productNames = [];
this.contractData.cover.before.allowedPools = {};

for (let i = 0; i < productTypesCount; i++) {
const productType = await this.cover.productTypes(i);
const productTypeName = await this.cover.productTypeNames(i);
this.contractData.cover.before.productTypes.push(productType);
this.contractData.cover.before.productTypeNames.push(productTypeName);
}

this.contractData.cover.before.products = await this.cover.getProducts();

const productsCount = this.contractData.cover.before.products.length;

for (let i = 0; i < productsCount; i++) {
const productName = await this.cover.productNames(i);
this.contractData.cover.before.productNames.push(productName);

const allowedPools = [];
let hasAllowedPools = false;

if (!this.contractData.cover.before.products[i].isDeprecated) {
for (let j = 1; j <= stakingPoolCount; j++) {
const isAllowed = await this.cover.isPoolAllowed(i, j);

if (isAllowed) {
allowedPools.push(j);
} else {
hasAllowedPools = true;
}
}
}

if (hasAllowedPools) {
this.contractData.cover.before.allowedPools[i] = allowedPools;
}
}
});

it('Upgrade contracts', async function () {
const contractsBefore = await this.master.getInternalContracts();

// StakingPool.sol
this.stakingPool = await ethers.deployContract('StakingPool', [
this.stakingNFT.address,
this.nxm.address,
this.cover.address,
this.tokenController.address,
this.master.address,
this.stakingProducts.address,
]);

// Cover.sol
this.cover = await ethers.deployContract('Cover', [
this.coverNFT.address,
this.stakingNFT.address,
this.stakingPoolFactory.address,
this.stakingPool.address,
]);

const contractCodeAddressMapping = {
[ContractCode.Cover]: this.cover.address,
};
// NOTE: Do not manipulate the map between Object.keys and Object.values otherwise the ordering could go wrong
const codes = Object.keys(contractCodeAddressMapping).map(code => toUtf8Bytes(code));
const addresses = Object.values(contractCodeAddressMapping);

await submitGovernanceProposal(
PROPOSAL_CATEGORIES.upgradeMultipleContracts, // upgradeMultipleContracts(bytes2[],address[])
defaultAbiCoder.encode(['bytes2[]', 'address[]'], [codes, addresses]),
this.abMembers,
this.governance,
);

const contractsAfter = await this.master.getInternalContracts();

console.info('Upgrade Contracts before:', formatInternalContracts(contractsBefore));
console.info('Upgrade Contracts after:', formatInternalContracts(contractsAfter));

// Set references to proxy contracts
this.cover = await getContractByContractCode('Cover', ContractCode.Cover);
});

it('Compares storage of upgrade Cover contract', async function () {
const stakingPoolCount = (await this.stakingPoolFactory.stakingPoolCount()).toNumber();

const productTypesCount = await this.cover.productTypesCount();

this.contractData.cover.after.productTypes = [];
this.contractData.cover.after.productTypeNames = [];
this.contractData.cover.after.productNames = [];
this.contractData.cover.after.allowedPools = {};

for (let i = 0; i < productTypesCount; i++) {
const productType = await this.cover.productTypes(i);
const productTypeName = await this.cover.productTypeNames(i);

expect(this.contractData.cover.before.productTypes[i]).to.be.deep.equal(productType);
expect(this.contractData.cover.before.productTypeNames[i]).to.be.deep.equal(productTypeName);
}

this.contractData.cover.after.products = await this.cover.getProducts();

const productsCount = this.contractData.cover.after.products.length;

for (let i = 0; i < productsCount; i++) {
const productName = await this.cover.productNames(i);

expect(this.contractData.cover.before.products[i]).to.be.deep.equal(this.contractData.cover.after.products[i]);
expect(this.contractData.cover.before.productNames[i]).to.be.deep.equal(productName);

const allowedPools = [];
let hasAllowedPools = false;

if (!this.contractData.cover.after.products[i].isDeprecated) {
for (let j = 1; j <= stakingPoolCount; j++) {
const isAllowed = await this.cover.isPoolAllowed(i, j);

if (isAllowed) {
allowedPools.push(j);
} else {
hasAllowedPools = true;
}
}
}

if (hasAllowedPools) {
expect(this.contractData.cover.before.allowedPools[i]).to.be.deep.equal(allowedPools);
}
}
});

it('Process expirations on pool 8', async function () {
const poolAddress = await this.cover.stakingPool(8);
const pool = await ethers.getContractAt(abis.StakingPool, poolAddress);

await pool.processExpirations(true);
});

require('./basic-functionality-tests');
});
2 changes: 1 addition & 1 deletion test/integration/setup.js
Original file line number Diff line number Diff line change
Expand Up @@ -695,7 +695,7 @@ async function setup() {
GLOBAL_REWARDS_RATIO: BigNumber.from(5000), // 50%
COMMISSION_DENOMINATOR: BigNumber.from(10000),
TARGET_PRICE_DENOMINATOR: await stakingProducts.TARGET_PRICE_DENOMINATOR(),
ONE_NXM: await stakingPool.ONE_NXM(),
ONE_NXM: parseEther('1'),
NXM_PER_ALLOCATION_UNIT: await stakingPool.NXM_PER_ALLOCATION_UNIT(),
USDC_DECIMALS: usdcDecimals,
};
Expand Down

0 comments on commit 2f4745d

Please sign in to comment.