diff --git a/contracts/interfaces/OpynPricerInterface.sol b/contracts/interfaces/OpynPricerInterface.sol index 0bdabe8ec..9924a7e8e 100644 --- a/contracts/interfaces/OpynPricerInterface.sol +++ b/contracts/interfaces/OpynPricerInterface.sol @@ -2,7 +2,7 @@ pragma solidity 0.6.10; interface OpynPricerInterface { - function getPrice() external view returns (uint256); + function getPrice(address) external view returns (uint256); function getHistoricalPrice(uint80 _roundId) external view returns (uint256, uint256); } diff --git a/contracts/pricers/ManualPricer.sol b/contracts/pricers/ManualPricer.sol new file mode 100644 index 000000000..52808a90c --- /dev/null +++ b/contracts/pricers/ManualPricer.sol @@ -0,0 +1,83 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity 0.6.10; + +import {OracleInterface} from "../interfaces/OracleInterface.sol"; +import {OpynPricerInterface} from "../interfaces/OpynPricerInterface.sol"; +import {SafeMath} from "../packages/oz/SafeMath.sol"; + +/** + * @notice A Pricer contract for one asset as reported by Manual entity + */ +contract ManualPricer is OpynPricerInterface { + using SafeMath for uint256; + + /// @notice the opyn oracle address + OracleInterface public oracle; + + /// @notice asset that this pricer will a get price for + address public asset; + /// @notice bot address that is allowed to call setExpiryPriceInOracle + address public bot; + + /// @notice lastExpiryTimestamp last timestamp that asset price was set + uint256 public lastExpiryTimestamp; + + /// @notice historicalPrice mapping of timestamp to price + mapping(uint256 => uint256) public historicalPrice; + + /** + * @param _bot priveleged address that can call setExpiryPriceInOracle + * @param _asset asset that this pricer will get a price for + * @param _oracle Opyn Oracle address + */ + constructor( + address _bot, + address _asset, + address _oracle + ) public { + require(_bot != address(0), "ManualPricer: Cannot set 0 address as bot"); + require(_oracle != address(0), "ManualPricer: Cannot set 0 address as oracle"); + + bot = _bot; + oracle = OracleInterface(_oracle); + asset = _asset; + } + + /** + * @notice modifier to check if sender address is equal to bot address + */ + modifier onlyBot() { + require(msg.sender == bot, "ManualPricer: unauthorized sender"); + + _; + } + + /** + * @notice set the expiry price in the oracle, can only be called by Bot address + * @param _expiryTimestamp expiry to set a price for + * @param _price the price of the asset + */ + function setExpiryPriceInOracle(uint256 _expiryTimestamp, uint256 _price) external onlyBot { + lastExpiryTimestamp = _expiryTimestamp; + historicalPrice[_expiryTimestamp] = _price; + oracle.setExpiryPrice(asset, _expiryTimestamp, _price); + } + + /** + * @notice get the live price for the asset + * @dev overides the getPrice function in OpynPricerInterface + * @return price of the asset in USD, scaled by 1e8 + */ + function getPrice(address) external view override returns (uint256) { + return historicalPrice[lastExpiryTimestamp]; + } + + /** + * @notice get historical chainlink price + * @param _roundId chainlink round id + * @return round price and timestamp + */ + function getHistoricalPrice(uint80 _roundId) external view override returns (uint256, uint256) { + revert("ManualPricer: Deprecated"); + } +} diff --git a/scripts/deployManualPricer.js b/scripts/deployManualPricer.js new file mode 100644 index 000000000..d2f87c9ea --- /dev/null +++ b/scripts/deployManualPricer.js @@ -0,0 +1,33 @@ +const yargs = require('yargs') + +const ManualPricer = artifacts.require('ManualPricer.sol') + +module.exports = async function(callback) { + try { + const options = yargs + .usage( + 'Usage: --network --bot --asset --oracle --gasPrice --gasLimit ', + ) + .option('network', {describe: 'Network name', type: 'string', demandOption: true}) + .option('bot', {describe: 'Bot address', type: 'string', demandOption: true}) + .option('asset', {describe: 'Asset 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 manual pricer contract on ${options.network} 🍕`) + + const tx = await ManualPricer.new(options.bot, options.asset, options.oracle, { + gasPrice: options.gasPrice, + gas: options.gasLimit, + }) + + console.log('Manual pricer deployed! 🎉') + console.log(`Transaction hash: ${tx.transactionHash}`) + console.log(`Deployed contract address: ${tx.address}`) + + callback() + } catch (err) { + callback(err) + } +} diff --git a/test/unit-tests/manualPricer.test.ts b/test/unit-tests/manualPricer.test.ts new file mode 100644 index 000000000..9380c7138 --- /dev/null +++ b/test/unit-tests/manualPricer.test.ts @@ -0,0 +1,97 @@ +import { ManualPricerInstance, MockOracleInstance, MockERC20Instance } from '../../build/types/truffle-types' + +import { createTokenAmount } from '../utils' +const { expectRevert, time } = require('@openzeppelin/test-helpers') + +const ManualPricer = artifacts.require('ManualPricer.sol') +const MockOracle = artifacts.require('MockOracle.sol') +const MockERC20 = artifacts.require('MockERC20.sol') + +// address(0) +const ZERO_ADDR = '0x0000000000000000000000000000000000000000' + +contract('ManualPricer', ([owner, bot, random]) => { + let oracle: MockOracleInstance + let weth: MockERC20Instance + // otoken + let pricer: ManualPricerInstance + + before('Deployment', async () => { + // deploy mock contracts + oracle = await MockOracle.new({ from: owner }) + weth = await MockERC20.new('WETH', 'WETH', 18) + // deploy pricer + pricer = await ManualPricer.new(bot, weth.address, oracle.address) + }) + + describe('constructor', () => { + it('should set the config correctly', async () => { + const asset = await pricer.asset() + assert.equal(asset, weth.address) + const bot = await pricer.bot() + assert.equal(bot, bot) + const oracleModule = await pricer.oracle() + assert.equal(oracleModule, oracle.address) + }) + it('should revert if initializing oracle with 0 address', async () => { + await expectRevert(ManualPricer.new(bot, weth.address, ZERO_ADDR), 'ManualPricer: Cannot set 0 address as oracle') + }) + it('should revert if initializing bot with 0 address', async () => { + await expectRevert( + ManualPricer.new(ZERO_ADDR, weth.address, oracle.address), + 'ManualPricer: Cannot set 0 address as bot', + ) + }) + }) + + describe('getPrice', () => { + it('should return the new price after resetting answer', async () => { + const newPrice = createTokenAmount(400, 8) + await pricer.setExpiryPriceInOracle(1, newPrice, { from: bot }) + const price = await pricer.getPrice(oracle.address) + const expectedResult = createTokenAmount(400, 8) + assert.equal(price.toString(), expectedResult.toString()) + }) + }) + + describe('setExpiryPrice', () => { + const p1 = createTokenAmount(150.333, 8) + + it('should set the correct price to the oracle', async () => { + const expiryTimestamp = 5 + + await pricer.setExpiryPriceInOracle(expiryTimestamp, p1, { from: bot }) + const priceFromOracle = await oracle.getExpiryPrice(weth.address, expiryTimestamp) + const lastExpiryTimestamp = await pricer.lastExpiryTimestamp() + assert.equal(p1.toString(), priceFromOracle[0].toString()) + assert.equal(lastExpiryTimestamp.toString(), expiryTimestamp.toString()) + assert.equal((await pricer.historicalPrice(lastExpiryTimestamp)).toString(), p1.toString()) + }) + + it('should revert if sender is not bot address', async () => { + const expiryTimestamp = 5 + await expectRevert( + pricer.setExpiryPriceInOracle(expiryTimestamp, p1, { from: random }), + 'ManualPricer: unauthorized sender', + ) + }) + }) + + describe('get historical price', async () => { + let t0: number + // p0 = price at t0 ... etc + const p0 = createTokenAmount(100, 8) + + it('should return historical price with timestamp', async () => { + await pricer.setExpiryPriceInOracle(0, p0, { from: bot }) + const roundData = await pricer.historicalPrice(0) + + assert.equal(roundData.toString(), p0, 'Historical round price mismatch') + }) + + it('should revert when no data round available', async () => { + const invalidRoundId = 2 + assert.equal((await pricer.historicalPrice(2)).toString(), '0', 'Historical round timestamp mismatch') + }) + }) +}) diff --git a/truffle-config.js b/truffle-config.js index eca765019..edf5a9acb 100644 --- a/truffle-config.js +++ b/truffle-config.js @@ -60,19 +60,8 @@ module.exports = { confirmations: 2, timeoutBlocks: 50, skipDryRun: false, - gasPrice: 25000000000, - }, - avax: { - provider: () => new HDWalletProvider(mnemonic, 'https://api.avax.network/ext/bc/C/rpc'), - network_id: 1, + gasPrice: 100000000000, }, - fuji: { - provider: () => new HDWalletProvider(mnemonic, 'https://api.avax-test.network/ext/bc/C/rpc'), - network_id: 1, - gas: 8000000, - gasPrice: 250000000000, - skipDryRun: true, - } }, mocha: {