Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Rocket Pool ETH Pricer #456

Open
wants to merge 7 commits into
base: master
Choose a base branch
from
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
10 changes: 10 additions & 0 deletions contracts/interfaces/RETHInterface.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity 0.6.10;

interface RETHInterface {
function getExchangeRate() external view returns (uint256);

function getETHValue(uint256 rethAmount) external view returns (uint256);

function getRethValue(uint256 ethAmount) external view returns (uint256);
}
25 changes: 25 additions & 0 deletions contracts/mocks/MockRETHToken.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity 0.6.10;

import {ERC20Upgradeable} from "../packages/oz/upgradeability/ERC20Upgradeable.sol";

contract MockRETHToken is ERC20Upgradeable {
uint256 public ethPerToken;

constructor(string memory _name, string memory _symbol) public {
__ERC20_init_unchained(_name, _symbol);
_setupDecimals(18);
}

function mint(address account, uint256 amount) public {
_mint(account, amount);
}

function setEthPerToken(uint256 _ethPerToken) external {
ethPerToken = _ethPerToken;
}

function getExchangeRate() external returns (uint256) {
return ethPerToken;
}
}
92 changes: 92 additions & 0 deletions contracts/pricers/RethPricer.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity =0.6.10;

import {OracleInterface} from "../interfaces/OracleInterface.sol";
import {OpynPricerInterface} from "../interfaces/OpynPricerInterface.sol";
import {RETHInterface} from "../interfaces/RETHInterface.sol";
import {SafeMath} from "../packages/oz/SafeMath.sol";

/**
* Error Codes
* W1: cannot deploy pricer, rETH address cannot be 0
* W2: cannot deploy pricer, underlying address cannot be 0
* W3: cannot deploy pricer, oracle address cannot be 0
* W4: cannot retrieve price, underlying price is 0
* W5: cannot set expiry price in oracle, underlying price is 0 and has not been set
* W6: cannot retrieve historical prices, getHistoricalPrice has been deprecated
*/

/**
* @title RethPricer
* @author Opyn Team
* @notice A Pricer contract for a rETH token
*/
contract RethPricer is OpynPricerInterface {
using SafeMath for uint256;

/// @notice opyn oracle address
OracleInterface public oracle;

/// @notice rETH token
RETHInterface public rETH;

/// @notice underlying asset (WETH)
address public underlying;

/**
* @param _rETH rETH
* @param _underlying underlying asset for rETH
* @param _oracle Opyn Oracle contract address
*/
constructor(
address _rETH,
address _underlying,
address _oracle
) public {
require(_rETH != address(0), "W1");
require(_underlying != address(0), "W2");
require(_oracle != address(0), "W3");

rETH = RETHInterface(_rETH);
oracle = OracleInterface(_oracle);
underlying = _underlying;
}

/**
* @notice get the live price for the asset
* @dev overrides the getPrice function in OpynPricerInterface
* @return price of 1 rETH in USD, scaled by 1e8
*/
function getPrice() external view override returns (uint256) {
uint256 underlyingPrice = oracle.getPrice(underlying);
require(underlyingPrice > 0, "W4");
return _underlyingPriceToRethPrice(underlyingPrice);
}

/**
* @notice set the expiry price in the oracle
* @dev requires that the underlying price has been set before setting a rETH price
* @param _expiryTimestamp expiry to set a price for
*/
function setExpiryPriceInOracle(uint256 _expiryTimestamp) external {
(uint256 underlyingPriceExpiry, ) = oracle.getExpiryPrice(underlying, _expiryTimestamp);
require(underlyingPriceExpiry > 0, "W5");
uint256 rEthPrice = _underlyingPriceToRethPrice(underlyingPriceExpiry);
oracle.setExpiryPrice(address(rETH), _expiryTimestamp, rEthPrice);
}

/**
* @dev convert underlying price to rETH price
* @param _underlyingPrice price of 1 underlying token (ie 1e18 WETH) in USD, scaled by 1e8
* @return price of 1 rETH in USD, scaled by 1e8
*/
function _underlyingPriceToRethPrice(uint256 _underlyingPrice) private view returns (uint256) {
uint256 ethPerReth = rETH.getExchangeRate();

return ethPerReth.mul(_underlyingPrice).div(1e18);
}

function getHistoricalPrice(uint80) external view override returns (uint256, uint256) {
revert("W6");
}
}
33 changes: 33 additions & 0 deletions scripts/deployRethPricer.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
const yargs = require('yargs')

const RethPricer = artifacts.require('RethPricer.sol')

module.exports = async function (callback) {
try {
const options = yargs
.usage(
'Usage: --network <network> --rETH <rETH> --underlying <underlying> --oracle <oracle> --gasPrice <gasPrice> --gasLimit <gasLimit>',
)
.option('network', { describe: 'Network name', type: 'string', demandOption: true })
.option('rETH', { describe: 'rETH address', type: 'string', demandOption: true })
.option('underlying', { describe: 'Underlying address', type: 'string', demandOption: true })
.option('oracle', { describe: 'Oracle module address', type: 'string', demandOption: true })
.option('gasPrice', { describe: 'Gas price in WEI', type: 'string', demandOption: false })
.option('gasLimit', { describe: 'Gas Limit in WEI', type: 'string', demandOption: false }).argv

console.log(`Deploying rETH pricer contract to ${options.network} 🍕`)

const tx = await RethPricer.new(options.rETH, options.underlying, options.oracle, {
gasPrice: options.gasPrice,
gas: options.gasLimit,
})

console.log('rETH pricer deployed! 🎉')
console.log(`Transaction hash: ${tx.transactionHash}`)
console.log(`Deployed contract address: ${tx.address}`)

callback()
} catch (err) {
callback(err)
}
}
122 changes: 122 additions & 0 deletions test/unit-tests/rETHPricer.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
import {
MockPricerInstance,
MockOracleInstance,
MockERC20Instance,
MockRETHTokenInstance,
RethPricerInstance,
} from '../../build/types/truffle-types'

import { underlyingPriceToYTokenPrice } from '../utils'

import BigNumber from 'bignumber.js'
import { createScaledNumber } from '../utils'
const { expectRevert, time } = require('@openzeppelin/test-helpers')

const MockPricer = artifacts.require('MockPricer.sol')
const MockOracle = artifacts.require('MockOracle.sol')

const MockERC20 = artifacts.require('MockERC20.sol')
const MockRETHToken = artifacts.require('MockRETHToken.sol')
const RethPricer = artifacts.require('RethPricer.sol')

// address(0)
const ZERO_ADDR = '0x0000000000000000000000000000000000000000'

contract('RethPricer', ([owner, random]) => {
let oracle: MockOracleInstance
let weth: MockERC20Instance
let rETH: MockRETHTokenInstance
// old pricer
let wethPricer: MockPricerInstance
// steth pricer
let rethPricer: RethPricerInstance

before('Deployment', async () => {
// deploy mock contracts
oracle = await MockOracle.new({ from: owner })
weth = await MockERC20.new('WETH', 'WETH', 18)
rETH = await MockRETHToken.new('rETH', 'rETH')
// mock underlying pricers
wethPricer = await MockPricer.new(weth.address, oracle.address)

await oracle.setAssetPricer(weth.address, wethPricer.address)
})

describe('constructor', () => {
it('should deploy the contract successfully with correct values', async () => {
rethPricer = await RethPricer.new(rETH.address, weth.address, oracle.address)

assert.equal(await rethPricer.rETH(), rETH.address)
assert.equal(await rethPricer.underlying(), weth.address)
assert.equal(await rethPricer.oracle(), oracle.address)
})

it('should revert if initializing with rETH = 0', async () => {
await expectRevert(RethPricer.new(ZERO_ADDR, weth.address, oracle.address), 'W1')
})

it('should revert if initializing with underlying = 0 address', async () => {
await expectRevert(RethPricer.new(rETH.address, ZERO_ADDR, oracle.address), 'W2')
})

it('should revert if initializing with oracle = 0 address', async () => {
await expectRevert(RethPricer.new(rETH.address, weth.address, ZERO_ADDR), 'W3')
})
})

describe('getPrice for rETH', () => {
const ethPrice = createScaledNumber(470)
const pricePerShare = new BigNumber('1009262845672227655')
before('mock data in chainlink pricer and rETH', async () => {
await oracle.setRealTimePrice(weth.address, ethPrice)
// await wethPricer.setPrice(ethPrice)
await rETH.setEthPerToken(pricePerShare)
})
it('should return the price in 1e8', async () => {
// how much 1e8 yToken worth in USD
const rETHprice = await rethPricer.getPrice()
const expectResult = await underlyingPriceToYTokenPrice(new BigNumber(ethPrice), pricePerShare, weth)
assert.equal(rETHprice.toString(), expectResult.toString())
// hard coded answer
// 1 yWETH = 9.4 USD
assert.equal(rETHprice.toString(), '47435353746')
})

it('should return the new price after resetting answer in underlying pricer', async () => {
const newPrice = createScaledNumber(500)
// await wethPricer.setPrice(newPrice)
await oracle.setRealTimePrice(weth.address, newPrice)
const rETHprice = await rethPricer.getPrice()
const expectedResult = await underlyingPriceToYTokenPrice(new BigNumber(newPrice), pricePerShare, weth)
assert.equal(rETHprice.toString(), expectedResult.toString())
})

it('should revert if price is lower than 0', async () => {
// await wethPricer.setPrice('0')
await oracle.setRealTimePrice(weth.address, '0')
await expectRevert(rethPricer.getPrice(), 'W4')
})
})

describe('setExpiryPrice', () => {
let expiry: number
const ethPrice = new BigNumber(createScaledNumber(300))
const pricePerShare = new BigNumber('1009262845672227655')

before('setup oracle record for weth price', async () => {
expiry = (await time.latest()) + time.duration.days(30).toNumber()
})

it("should revert if oracle don't have price of underlying yet", async () => {
await expectRevert(rethPricer.setExpiryPriceInOracle(expiry), 'W5')
})

it('should set price successfully by arbitrary address', async () => {
await oracle.setExpiryPrice(weth.address, expiry, ethPrice)
await rethPricer.setExpiryPriceInOracle(expiry, { from: random })
const [price] = await oracle.getExpiryPrice(rETH.address, expiry)
const expectedResult = await underlyingPriceToYTokenPrice(ethPrice, pricePerShare, weth)
assert.equal(price.toString(), expectedResult.toString())
})
})
})