Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions contracts/interfaces/IPool.sol
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,8 @@ interface IPool {

function setAssetDetails(uint assetId, bool isCoverAsset, bool isAbandoned) external;

function setAssetOracle(address assetAddress, address aggregator, AggregatorType aggregatorType) external;

function sendPayout(uint assetIndex, address payable payoutAddress, uint amount, uint depositInETH) external;

function sendEth(address payable payoutAddress, uint amount) external;
Expand Down Expand Up @@ -113,6 +115,7 @@ interface IPool {

// price feed
error AggregatorMustNotBeZeroAddress();
error AggregatorAssetMustNotBeETH();
error IncompatibleAggregatorDecimals(address aggregator, uint expectedDecimals, uint aggregatorDecimals);
error InvalidEthAggregatorType(AggregatorType actual, AggregatorType expected);
error NonPositiveRate(address aggregator, int rate);
Expand Down
4 changes: 4 additions & 0 deletions contracts/mocks/generic/PoolGeneric.sol
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,10 @@ contract PoolGeneric is IPool {
revert("transferAssetToSwapOperator unsupported");
}

function setAssetOracle(address /* assetAddress */, address /* aggregator */, AggregatorType /* aggregatorType */) external virtual {
revert("setAssetOracle unsupported");
}

function sendPayout(uint /* assetIndex */, address payable /* payoutAddress */, uint /* amount */, uint /* depositInETH */) external virtual {
revert("sendPayout unsupported");
}
Expand Down
22 changes: 22 additions & 0 deletions contracts/modules/capital/AggregatorRETH.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
// SPDX-License-Identifier: GPL-3.0-only

pragma solidity ^0.8.18;

interface IRETH {
function getExchangeRate() external view returns (uint256);
}

contract AggregatorRETH {

uint8 public decimals = 18;
IRETH public immutable rETH;

constructor(address _rETH) {
rETH = IRETH(_rETH);
}

function latestAnswer() public view returns (int256) {
return int256(rETH.getExchangeRate());
}

}
24 changes: 24 additions & 0 deletions contracts/modules/capital/Pool.sol
Original file line number Diff line number Diff line change
Expand Up @@ -480,6 +480,30 @@ contract Pool is IPool, ReentrancyGuard, RegistryAware {
return uint(rate) * 1 ether / uint(ethUsdRate);
}

function setAssetOracle(address assetAddress, address aggregator, AggregatorType aggregatorType) external onlyContracts(C_GOVERNOR) {
require(assetAddress != address(0), AssetMustNotBeZeroAddress());
require(assetAddress != ETH, AggregatorAssetMustNotBeETH());
require(aggregator != address(0), AggregatorMustNotBeZeroAddress());

// require asset to be registered in the pool (reverts AssetNotFound otherwise)
getAssetId(assetAddress);
uint assetDecimals = IERC20Metadata(assetAddress).decimals();
uint8 aggregatorDecimals = Aggregator(aggregator).decimals();

if (aggregatorType == AggregatorType.ETH && aggregatorDecimals != 18) {
revert IncompatibleAggregatorDecimals(address(aggregator), 18, aggregatorDecimals);
}
if (aggregatorType == AggregatorType.USD && aggregatorDecimals != 8) {
revert IncompatibleAggregatorDecimals(address(aggregator), 8, aggregatorDecimals);
}

oracles[assetAddress] = Oracle({
aggregator: Aggregator(aggregator),
aggregatorType: aggregatorType,
assetDecimals: assetDecimals.toUint8()
});
}

/* ========== MIGRATION ========== */

function migrate(
Expand Down
139 changes: 139 additions & 0 deletions test/fork/rEthOracle/rETH.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
const { ethers, network, nexus, tracer } = require('hardhat');
const { expect } = require('chai');
const { abis, addresses } = require('@nexusmutual/deployments');
const { setBalance, takeSnapshot } = require('@nomicfoundation/hardhat-network-helpers');
const {
revertToSnapshot,
Addresses,
createSafeExecutor,
getFundedSigner,
executeGovernorProposal,
} = require('../utils');
const rETHAbi = require('./rETHAbi.json');

const { deployContract, parseEther, formatEther } = ethers;
const { ContractIndexes } = nexus.constants;

describe('Pool - rETH oracle change', function () {
before(async function () {
// Get or revert snapshot if network is tenderly
if (network.name === 'tenderly') {
const { TENDERLY_SNAPSHOT_ID } = process.env;
if (TENDERLY_SNAPSHOT_ID) {
await revertToSnapshot(TENDERLY_SNAPSHOT_ID);
console.info(`Reverted to snapshot ${TENDERLY_SNAPSHOT_ID}`);
} else {
const { snapshotId } = await takeSnapshot();
console.info('Snapshot ID: ', snapshotId);
}
}

const [deployer] = await ethers.getSigners();
await setBalance(deployer.address, parseEther('1000'));
});

it('load contracts', async function () {
this.registry = await ethers.getContractAt(abis.Registry, addresses.Registry);
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.coverProducts = await ethers.getContractAt(abis.CoverProducts, addresses.CoverProducts);
this.pool = await ethers.getContractAt(abis.Pool, addresses.Pool);
this.safeTracker = await ethers.getContractAt(abis.SafeTracker, addresses.SafeTracker);
this.assessments = await ethers.getContractAt(abis.Assessments, addresses.Assessments);
this.claims = await ethers.getContractAt(abis.Claims, addresses.Claims);
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.tokenController = await ethers.getContractAt(abis.TokenController, addresses.TokenController);
this.individualClaims = await ethers.getContractAt(abis.Claims, addresses.Claims);
this.stakingPoolFactory = await ethers.getContractAt(abis.StakingPoolFactory, addresses.StakingPoolFactory);
this.ramm = await ethers.getContractAt(abis.Ramm, addresses.Ramm);
this.limitOrders = await ethers.getContractAt(abis.LimitOrders, addresses.LimitOrders);
this.governor = await ethers.getContractAt(abis.Governor, addresses.Governor);
this.assessmentsViewer = await ethers.getContractAt(abis.Assessments, addresses.Assessments);
this.coverViewer = await ethers.getContractAt(abis.CoverViewer, addresses.CoverViewer);
this.stakingViewer = await ethers.getContractAt(abis.StakingViewer, addresses.StakingViewer);

// External contracts
this.coverBroker = await ethers.getContractAt(abis.CoverBroker, addresses.CoverBroker);

// Token Mocks
this.weth = await ethers.getContractAt('WETH9', Addresses.WETH_ADDRESS);
this.cbBTC = await ethers.getContractAt('ERC20Mock', Addresses.CBBTC_ADDRESS);
this.dai = await ethers.getContractAt('ERC20Mock', Addresses.DAI_ADDRESS);
this.usdc = await ethers.getContractAt('ERC20Mock', Addresses.USDC_ADDRESS);
this.rEth = await ethers.getContractAt(rETHAbi, Addresses.RETH_ADDRESS);
this.stEth = await ethers.getContractAt('ERC20Mock', Addresses.STETH_ADDRESS);
this.awEth = await ethers.getContractAt('ERC20Mock', Addresses.AWETH_ADDRESS);
this.enzymeShares = await ethers.getContractAt('ERC20Mock', Addresses.ENZYMEV4_VAULT_PROXY_ADDRESS);

// safe executor
this.executeSafeTransaction = await createSafeExecutor(Addresses.ADVISORY_BOARD_MULTISIG);

Object.entries(addresses).forEach(([k, v]) => (tracer.nameTags[v] = `#[${k}]`));
});

it('Impersonate AB members', async function () {
const boardSeats = await this.registry.ADVISORY_BOARD_SEATS();
this.abMembers = [];
for (let i = 1; i <= boardSeats; i++) {
const address = await this.registry.getMemberAddressBySeat(i);
this.abMembers.push(await getFundedSigner(address));
}
});

it('Collect storage data before upgrade', async function () {
this.poolData = {};
this.poolData.assets = await this.pool.getAssets();
this.poolData.mcr = await this.pool.getMCR();
this.poolData.mcrRatio = await this.pool.getMCRRatio();
this.rate = await this.pool.getEthForAsset(this.rEth.target, parseEther('1'));
});

it('Upgrade Pool contracts and change rETH oracle', async function () {
this.rETHAggregator = await deployContract('AggregatorRETH', [this.rEth]);
const pool = await deployContract('Pool', [this.registry.target]);

const transactions = [
{
target: this.registry,
value: 0n,
data: this.registry.interface.encodeFunctionData('upgradeContract', [ContractIndexes.C_POOL, pool.target]),
},
{
target: this.pool,
value: 0n,
data: pool.interface.encodeFunctionData('setAssetOracle', [
this.rEth.target,
this.rETHAggregator.target,
nexus.constants.AggregatorType.ETH,
]),
},
];

await executeGovernorProposal(this.governor, this.abMembers, transactions);

this.pool = await ethers.getContractAt('Pool', pool.target);

const assets = await this.pool.getAssets();
const mcr = await this.pool.getMCR();
const mcrRatio = await this.pool.getMCRRatio();
const rEthOracle = await this.pool.oracles(this.rEth.target);

console.log('===================OLD VALUES=====================');
console.log('mcr', formatEther(this.poolData.mcr));
console.log('mcrRatio', formatEther(this.poolData.mcrRatio));
console.log('rate', formatEther(this.rate));
console.log('===================NEW VALUES=====================');
console.log('mcr', formatEther(mcr));
console.log('mcrRatio', formatEther(mcrRatio));
console.log('rate', formatEther(await this.rEth.getExchangeRate()));

expect(this.poolData.assets).to.be.deep.equal(assets);
expect(rEthOracle.aggregator).to.be.deep.equal(this.rETHAggregator.target);
});

require('../basic-functionality-tests');
});
Loading
Loading