From 94642b58259e1880a6b79bea3caeb256dc475ccd Mon Sep 17 00:00:00 2001 From: Jamie Pickett Date: Mon, 18 Mar 2024 17:51:37 -0400 Subject: [PATCH] add tests, finish contract, and update readme --- README.md | 2 +- .../oracles/custom/MysoOracle.sol | 47 +++- hardhat.config.ts | 11 + .../mainnet-myso-oracle-forked-tests.ts | 261 ++++++++++++++++++ 4 files changed, 316 insertions(+), 5 deletions(-) create mode 100644 test/peer-to-peer/mainnet-myso-oracle-forked-tests.ts diff --git a/README.md b/README.md index b5934c27..57618d01 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ The protocol supports two different models, each targeted at different use cases ## Quick Start ``` npm i -npx hardhat test +npx hardhat test --grep "IOO price correctly" ``` ## Contract Files diff --git a/contracts/peer-to-peer/oracles/custom/MysoOracle.sol b/contracts/peer-to-peer/oracles/custom/MysoOracle.sol index 1621f79f..1869ff39 100644 --- a/contracts/peer-to-peer/oracles/custom/MysoOracle.sol +++ b/contracts/peer-to-peer/oracles/custom/MysoOracle.sol @@ -3,11 +3,11 @@ pragma solidity 0.8.19; import {ChainlinkBase} from "../chainlink/ChainlinkBase.sol"; -import {Errors} from "../../../Errors.sol"; import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; import {Ownable2Step} from "@openzeppelin/contracts/access/Ownable2Step.sol"; import {IWSTETH} from "../../interfaces/oracles/IWSTETH.sol"; import {Math} from "@openzeppelin/contracts/utils/math/Math.sol"; +import {IERC20Metadata} from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol"; /** * @dev supports oracles which are compatible with v2v3 or v3 interfaces @@ -24,11 +24,11 @@ contract MysoOracle is ChainlinkBase, Ownable2Step { address internal constant WETH = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2; // weth address internal constant WSTETH = 0x7f39C581F595B53c5cb19bD0b3f8dA6c935E2Ca0; //wsteth - address internal constant STETH = - 0xae7ab96520DE3A18E5e111B5EaAb095312D7fE84; //steth uint256 internal constant MYSO_IOO_BASE_CURRENCY_UNIT = 1e18; // 18 decimals for ETH based oracles address internal constant ETH_USD_CHAINLINK = 0x5f4eC3Df9cbd43714FE2740f5E3616155c5b8419; //eth usd chainlink + address internal constant STETH_ETH_CHAINLINK = + 0x86392dC19c0b719886221c78AB11eb8Cf5c52812; //steth eth chainlink uint256 internal constant MYSO_PRICE_TIME_LOCK = 1 hours; @@ -40,6 +40,8 @@ contract MysoOracle is ChainlinkBase, Ownable2Step { uint32 switchTime ); + error NoMyso(); + /** * @dev constructor for MysoOracle * @param _tokenAddrs array of token addresses @@ -97,6 +99,41 @@ contract MysoOracle is ChainlinkBase, Ownable2Step { } } + function getPrice( + address collToken, + address loanToken + ) external view override returns (uint256 collTokenPriceInLoanToken) { + (uint256 priceOfCollToken, uint256 priceOfLoanToken) = getRawPrices( + collToken, + loanToken + ); + uint256 loanTokenDecimals = (loanToken == MYSO) + ? 18 + : IERC20Metadata(loanToken).decimals(); + collTokenPriceInLoanToken = + (priceOfCollToken * 10 ** loanTokenDecimals) / + priceOfLoanToken; + } + + function getRawPrices( + address collToken, + address loanToken + ) + public + view + override + returns (uint256 collTokenPriceRaw, uint256 loanTokenPriceRaw) + { + // must have at least one token is MYSO to use this oracle + if (collToken != MYSO && loanToken != MYSO) { + revert NoMyso(); + } + (collTokenPriceRaw, loanTokenPriceRaw) = ( + _getPriceOfToken(collToken), + _getPriceOfToken(loanToken) + ); + } + function _getPriceOfToken( address token ) internal view virtual override returns (uint256 tokenPriceRaw) { @@ -113,7 +150,9 @@ contract MysoOracle is ChainlinkBase, Ownable2Step { function _getWstEthPrice() internal view returns (uint256 wstEthPriceRaw) { uint256 stEthAmountPerWstEth = IWSTETH(WSTETH).getStETHByWstETH(1e18); - uint256 stEthPriceInEth = _getPriceOfToken(STETH); + uint256 stEthPriceInEth = _checkAndReturnLatestRoundData( + (STETH_ETH_CHAINLINK) + ); wstEthPriceRaw = Math.mulDiv( stEthPriceInEth, stEthAmountPerWstEth, diff --git a/hardhat.config.ts b/hardhat.config.ts index d44dea70..82dc26f1 100644 --- a/hardhat.config.ts +++ b/hardhat.config.ts @@ -28,6 +28,17 @@ export const getRecentMainnetForkingConfig = () => { return { chainId: chainId, url: url, blockNumber: blockNumber } } +export const getMysoOracleMainnetForkingConfig = () => { + const INFURA_API_KEY = process.env.INFURA_API_KEY + if (INFURA_API_KEY === undefined) { + throw new Error('Invalid hardhat.config.ts! Need to set `INFURA_API_KEY`!') + } + const chainId = 1 + const url = `https://mainnet.infura.io/v3/${INFURA_API_KEY}` + const blockNumber = 19300000 // 2024-02-24 (9PM UTC) + return { chainId: chainId, url: url, blockNumber: blockNumber } +} + export const getArbitrumForkingConfig = () => { const ALCHEMY_API_KEY = process.env.ALCHEMY_API_KEY if (ALCHEMY_API_KEY === undefined) { diff --git a/test/peer-to-peer/mainnet-myso-oracle-forked-tests.ts b/test/peer-to-peer/mainnet-myso-oracle-forked-tests.ts new file mode 100644 index 00000000..40909f6f --- /dev/null +++ b/test/peer-to-peer/mainnet-myso-oracle-forked-tests.ts @@ -0,0 +1,261 @@ +import { expect } from 'chai' +import { ethers } from 'hardhat' +import { HARDHAT_CHAIN_ID_AND_FORKING_CONFIG, getMysoOracleMainnetForkingConfig } from '../../hardhat.config' + +// test config constants & vars +let snapshotId: String // use snapshot id to reset state before each test + +// constants +const hre = require('hardhat') +const BASE = ethers.BigNumber.from(10).pow(18) +const ONE_USDC = ethers.BigNumber.from(10).pow(6) +const ONE_WETH = ethers.BigNumber.from(10).pow(18) +const ONE_MYSO = ethers.BigNumber.from(10).pow(18) +const ONE_WSTETH = ethers.BigNumber.from(10).pow(18) +const MAX_UINT128 = ethers.BigNumber.from(2).pow(128).sub(1) +const MAX_UINT256 = ethers.BigNumber.from(2).pow(256).sub(1) +const ONE_HOUR = ethers.BigNumber.from(60 * 60) +const ZERO_ADDR = '0x0000000000000000000000000000000000000000' +const ZERO_BYTES32 = ethers.utils.formatBytes32String('') + +describe('Peer-to-Peer: Myso Recent Forked Mainnet Tests', function () { + before(async () => { + console.log('Note: Running mainnet tests with the following forking config:') + console.log(HARDHAT_CHAIN_ID_AND_FORKING_CONFIG) + if (HARDHAT_CHAIN_ID_AND_FORKING_CONFIG.chainId !== 1) { + console.warn('Invalid hardhat forking config! Expected `HARDHAT_CHAIN_ID_AND_FORKING_CONFIG.chainId` to be 1!') + + console.warn('Assuming that current test run is using `npx hardhat coverage`!') + + console.warn('Re-importing mainnet forking config from `hardhat.config.ts`...') + const mainnetForkingConfig = getMysoOracleMainnetForkingConfig() + + console.warn('Overwriting chainId to hardhat default `31337` to make off-chain signing consistent...') + HARDHAT_CHAIN_ID_AND_FORKING_CONFIG.chainId = 31337 + + console.log('block number: ', mainnetForkingConfig.url) + + console.warn('Trying to manually switch network to forked mainnet for this test file...') + await hre.network.provider.request({ + method: 'hardhat_reset', + params: [ + { + forking: { + jsonRpcUrl: mainnetForkingConfig.url, + blockNumber: mainnetForkingConfig.blockNumber + } + } + ] + }) + } + }) + + beforeEach(async () => { + snapshotId = await hre.network.provider.send('evm_snapshot') + }) + + afterEach(async () => { + await hre.network.provider.send('evm_revert', [snapshotId]) + }) + + async function setupTest() { + const [lender, signer, borrower, team, whitelistAuthority, someUser] = await ethers.getSigners() + /* ************************************ */ + /* DEPLOYMENT OF SYSTEM CONTRACTS START */ + /* ************************************ */ + // deploy address registry + const AddressRegistry = await ethers.getContractFactory('AddressRegistry') + const addressRegistry = await AddressRegistry.connect(team).deploy() + await addressRegistry.deployed() + + // deploy borrower gate way + const BorrowerGateway = await ethers.getContractFactory('BorrowerGateway') + const borrowerGateway = await BorrowerGateway.connect(team).deploy(addressRegistry.address) + await borrowerGateway.deployed() + + // deploy quote handler + const QuoteHandler = await ethers.getContractFactory('QuoteHandler') + const quoteHandler = await QuoteHandler.connect(team).deploy(addressRegistry.address) + await quoteHandler.deployed() + + // deploy lender vault implementation + const LenderVaultImplementation = await ethers.getContractFactory('LenderVaultImpl') + const lenderVaultImplementation = await LenderVaultImplementation.connect(team).deploy() + await lenderVaultImplementation.deployed() + + // deploy LenderVaultFactory + const LenderVaultFactory = await ethers.getContractFactory('LenderVaultFactory') + const lenderVaultFactory = await LenderVaultFactory.connect(team).deploy( + addressRegistry.address, + lenderVaultImplementation.address + ) + await lenderVaultFactory.deployed() + + // initialize address registry + await addressRegistry.connect(team).initialize(lenderVaultFactory.address, borrowerGateway.address, quoteHandler.address) + + /* ********************************** */ + /* DEPLOYMENT OF SYSTEM CONTRACTS END */ + /* ********************************** */ + + // create a vault + await lenderVaultFactory.connect(lender).createVault(ZERO_BYTES32) + const lenderVaultAddrs = await addressRegistry.registeredVaults() + const lenderVaultAddr = lenderVaultAddrs[0] + const lenderVault = await LenderVaultImplementation.attach(lenderVaultAddr) + + // prepare WETH balance + const WETH_ADDRESS = '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2' + const weth = await ethers.getContractAt('IWETH', WETH_ADDRESS) + await ethers.provider.send('hardhat_setBalance', [borrower.address, '0x204FCE5E3E25026110000000']) + await weth.connect(borrower).deposit({ value: ONE_WETH.mul(1) }) + + //prepare wstEth balances + const WSTETH_ADDRESS = '0x7f39C581F595B53c5cb19bD0b3f8dA6c935E2Ca0' + const WSTETH_HOLDER = '0x5fEC2f34D80ED82370F733043B6A536d7e9D7f8d' + const wsteth = await ethers.getContractAt('IWETH', WSTETH_ADDRESS) + await ethers.provider.send('hardhat_setBalance', [WSTETH_HOLDER, '0x56BC75E2D63100000']) + await hre.network.provider.request({ + method: 'hardhat_impersonateAccount', + params: [WSTETH_HOLDER] + }) + + const wstEthHolder = await ethers.getSigner(WSTETH_HOLDER) + + await wsteth.connect(wstEthHolder).transfer(team.address, '10000000000000000000') + + const reth = '0xae78736Cd615f374D3085123A210448E74Fc6393' + const cbeth = '0xBe9895146f7AF43049ca1c1AE358B0541Ea49704' + const rethToEthChainlinkAddr = '0x536218f9E9Eb48863970252233c8F271f554C2d0' + const cbethToEthChainlinkAddr = '0xF017fcB346A1885194689bA23Eff2fE6fA5C483b' + + return { + addressRegistry, + borrowerGateway, + quoteHandler, + lenderVaultImplementation, + lender, + signer, + borrower, + team, + whitelistAuthority, + weth, + wsteth, + reth, + cbeth, + rethToEthChainlinkAddr, + cbethToEthChainlinkAddr, + lenderVault, + lenderVaultFactory, + someUser + } + } + + describe('Myso Oracle Testing', function () { + it('Should set up myso IOO price correctly', async function () { + const { + addressRegistry, + borrowerGateway, + quoteHandler, + lender, + borrower, + team, + weth, + wsteth, + reth, + cbeth, + cbethToEthChainlinkAddr, + rethToEthChainlinkAddr, + lenderVault + } = await setupTest() + + const myso = '0x00000000000000000000000000000000DeaDBeef' + + // deploy myso oracle + const MysoOracle = await ethers.getContractFactory('MysoOracle') + + const mysoOracle = await MysoOracle.connect(team).deploy( + [reth, cbeth], + [rethToEthChainlinkAddr, cbethToEthChainlinkAddr], + 50000000 + ) + await mysoOracle.deployed() + + const mysoPriceData = await mysoOracle.mysoPrice() + + expect(mysoPriceData.prePrice).to.equal(50000000) + expect(mysoPriceData.postPrice).to.equal(50000000) + const timestampAtDeployment = mysoPriceData.switchTime + + await expect(mysoOracle.connect(lender).setMysoPrice(80000000)).to.be.revertedWith('Ownable: caller is not the owner') + + await expect(mysoOracle.getPrice(weth.address, cbeth)).to.be.revertedWithCustomError(mysoOracle, 'NoMyso') + + const wethCollMysoLoanPrice = await mysoOracle.getPrice(weth.address, myso) + const wstEthCollMysoLoanPrice = await mysoOracle.getPrice(wsteth.address, myso) + const rethCollMysoLoanPrice = await mysoOracle.getPrice(reth, myso) + const cbethCollMysoLoanPrice = await mysoOracle.getPrice(cbeth, myso) + + //toggle to show logs + const showLogs = true + if (showLogs) { + console.log( + 'wethCollMysoLoanPrice', + Math.round(1000000 * Number(ethers.utils.formatUnits(wethCollMysoLoanPrice, 18).slice(0, 8))) / 1000000 + ) + console.log( + 'wstEthCollMysoLoanPrice', + Math.round(1000000 * Number(ethers.utils.formatUnits(wstEthCollMysoLoanPrice, 18).slice(0, 8))) / 1000000 + ) + console.log( + 'rethCollMysoLoanPrice', + Math.round(1000000 * Number(ethers.utils.formatUnits(rethCollMysoLoanPrice, 18).slice(0, 8))) / 1000000 + ) + console.log( + 'cbEthCollMysoLoanPrice', + Math.round(1000000 * Number(ethers.utils.formatUnits(cbethCollMysoLoanPrice, 18).slice(0, 8))) / 1000000 + ) + } + + await mysoOracle.connect(team).setMysoPrice(100000000) + const newMysoPriceData = await mysoOracle.mysoPrice() + expect(newMysoPriceData.prePrice).to.equal(50000000) + expect(newMysoPriceData.postPrice).to.equal(100000000) + expect(newMysoPriceData.switchTime).to.be.gte(ethers.BigNumber.from(timestampAtDeployment).add(ONE_HOUR)) + const newWethCollMysoLoanPrice = await mysoOracle.getPrice(weth.address, myso) + expect(newWethCollMysoLoanPrice).to.equal(wethCollMysoLoanPrice) + await ethers.provider.send('evm_mine', [ethers.BigNumber.from(newMysoPriceData.switchTime).add(10).toNumber()]) + const wethCollMysoLoanPostPrice = await mysoOracle.getPrice(weth.address, myso) + // difference is very small less than the order of 10^-13 + expect( + wethCollMysoLoanPostPrice + .sub(wethCollMysoLoanPrice.div(2)) + .mul(ethers.BigNumber.from(10).pow(13)) + .div(wethCollMysoLoanPostPrice) + ).to.be.equal(0) + + const wstEthCollMysoLoanPostPrice = await mysoOracle.getPrice(wsteth.address, myso) + const rethCollMysoLoanPostPrice = await mysoOracle.getPrice(reth, myso) + const cbethCollMysoLoanPostPrice = await mysoOracle.getPrice(cbeth, myso) + + if (showLogs) { + console.log( + 'wethCollMysoLoanPostPrice', + Math.round(1000000 * Number(ethers.utils.formatUnits(wethCollMysoLoanPostPrice, 18).slice(0, 8))) / 1000000 + ) + console.log( + 'wstEthCollMysoLoanPostPrice', + Math.round(1000000 * Number(ethers.utils.formatUnits(wstEthCollMysoLoanPostPrice, 18).slice(0, 8))) / 1000000 + ) + console.log( + 'rethCollMysoLoanPostPrice', + Math.round(1000000 * Number(ethers.utils.formatUnits(rethCollMysoLoanPostPrice, 18).slice(0, 8))) / 1000000 + ) + console.log( + 'cbEthCollMysoLoanPostPrice', + Math.round(1000000 * Number(ethers.utils.formatUnits(cbethCollMysoLoanPostPrice, 18).slice(0, 8))) / 1000000 + ) + } + }) + }) +})