Skip to content

Commit

Permalink
add tests, finish contract, and update readme
Browse files Browse the repository at this point in the history
  • Loading branch information
jpick713 committed Mar 18, 2024
1 parent 4ab992e commit 94642b5
Show file tree
Hide file tree
Showing 4 changed files with 316 additions and 5 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
47 changes: 43 additions & 4 deletions contracts/peer-to-peer/oracles/custom/MysoOracle.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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;

Expand All @@ -40,6 +40,8 @@ contract MysoOracle is ChainlinkBase, Ownable2Step {
uint32 switchTime
);

error NoMyso();

/**
* @dev constructor for MysoOracle
* @param _tokenAddrs array of token addresses
Expand Down Expand Up @@ -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) {
Expand All @@ -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,
Expand Down
11 changes: 11 additions & 0 deletions hardhat.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
261 changes: 261 additions & 0 deletions test/peer-to-peer/mainnet-myso-oracle-forked-tests.ts
Original file line number Diff line number Diff line change
@@ -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
)
}
})
})
})

0 comments on commit 94642b5

Please sign in to comment.