From 35c0f36800eb96024c67ac673937dd8c3b8b6db0 Mon Sep 17 00:00:00 2001 From: Your Name Date: Tue, 29 Nov 2022 14:27:08 -0800 Subject: [PATCH 1/8] Add ManualPricer --- contracts/pricers/ManualPricer.sol | 83 ++++++++++++++++++++ test/unit-tests/manualPricer.test.ts | 110 +++++++++++++++++++++++++++ 2 files changed, 193 insertions(+) create mode 100644 contracts/pricers/ManualPricer.sol create mode 100644 test/unit-tests/manualPricer.test.ts diff --git a/contracts/pricers/ManualPricer.sol b/contracts/pricers/ManualPricer.sol new file mode 100644 index 000000000..8b3da9a9c --- /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() 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/test/unit-tests/manualPricer.test.ts b/test/unit-tests/manualPricer.test.ts new file mode 100644 index 000000000..844977a06 --- /dev/null +++ b/test/unit-tests/manualPricer.test.ts @@ -0,0 +1,110 @@ +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, oracle.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) + const price = await pricer.getPrice() + const expectedResult = createTokenAmount(400, 8) + assert.equal(price.toString(), expectedResult.toString()) + }) + }) + + describe('setExpiryPrice', () => { + // time order: t0, t1, t2, t3, t4 + let t0: number, t1: number, t2: number, t3: number, t4: number + // p0 = price at t0 ... etc + const p0 = createTokenAmount(100, 8) + const p1 = createTokenAmount(150.333, 8) + const p2 = createTokenAmount(180, 8) + const p3 = createTokenAmount(200, 8) + const p4 = createTokenAmount(140, 8) + + it('should set the correct price to the oracle', async () => { + const expiryTimestamp = (t0 + t1) / 2 // between t0 and t1 + + await pricer.setExpiryPriceInOracle(expiryTimestamp, p1, { from: bot }) + const priceFromOracle = await oracle.getExpiryPrice(weth.address, expiryTimestamp) + const lastExpiryTimestamp = await oracle.lastExpiryTimestamp() + assert.equal(p1.toString(), priceFromOracle[0].toString()) + assert.equal(lastExpiryTimestamp, expiryTimestamp) + assert.equal(await oracle.historicalPrice(lastExpiryTimestamp), p1) + }) + + it('should revert if sender is not bot address', async () => { + const expiryTimestamp = (t1 + t2) / 2 // between t0 and t1 + const roundId = 1 + await expectRevert( + pricer.setExpiryPriceInOracle(expiryTimestamp, roundId, { 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[0].toString(), p0, 'Historical round price mismatch') + + assert.equal(roundData[1].toNumber(), t0, 'Historical round timestamp mismatch') + }) + + it('should revert when no data round available', async () => { + const invalidRoundId = 2 + assert.equal(pricer.historicalPrice(2), '0', 'Historical round timestamp mismatch') + }) + }) +}) From df3dacbcfe82d081d7afecf256e2a4eb410a236f Mon Sep 17 00:00:00 2001 From: Your Name Date: Tue, 29 Nov 2022 14:31:50 -0800 Subject: [PATCH 2/8] add manual pricer deployment script --- scripts/deployManualPricer.js | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 scripts/deployManualPricer.js diff --git a/scripts/deployManualPricer.js b/scripts/deployManualPricer.js new file mode 100644 index 000000000..19af98344 --- /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.aggregator, 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) + } +} From 41184f61d96205f21eb8e1d841a8203355cad893 Mon Sep 17 00:00:00 2001 From: Your Name Date: Tue, 29 Nov 2022 14:52:30 -0800 Subject: [PATCH 3/8] fix test --- test/unit-tests/manualPricer.test.ts | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/test/unit-tests/manualPricer.test.ts b/test/unit-tests/manualPricer.test.ts index 844977a06..2e52a581c 100644 --- a/test/unit-tests/manualPricer.test.ts +++ b/test/unit-tests/manualPricer.test.ts @@ -72,10 +72,10 @@ contract('ManualPricer', ([owner, bot, random]) => { await pricer.setExpiryPriceInOracle(expiryTimestamp, p1, { from: bot }) const priceFromOracle = await oracle.getExpiryPrice(weth.address, expiryTimestamp) - const lastExpiryTimestamp = await oracle.lastExpiryTimestamp() + const lastExpiryTimestamp = await pricer.lastExpiryTimestamp() assert.equal(p1.toString(), priceFromOracle[0].toString()) assert.equal(lastExpiryTimestamp, expiryTimestamp) - assert.equal(await oracle.historicalPrice(lastExpiryTimestamp), p1) + assert.equal(await pricer.historicalPrice(lastExpiryTimestamp), p1) }) it('should revert if sender is not bot address', async () => { @@ -97,14 +97,12 @@ contract('ManualPricer', ([owner, bot, random]) => { await pricer.setExpiryPriceInOracle(0, p0, { from: bot }) const roundData = await pricer.historicalPrice(0) - assert.equal(roundData[0].toString(), p0, 'Historical round price mismatch') - - assert.equal(roundData[1].toNumber(), t0, 'Historical round timestamp mismatch') + assert.equal(roundData.toString(), p0, 'Historical round price mismatch') }) it('should revert when no data round available', async () => { const invalidRoundId = 2 - assert.equal(pricer.historicalPrice(2), '0', 'Historical round timestamp mismatch') + assert.equal(pricer.historicalPrice(2).toString(), '0', 'Historical round timestamp mismatch') }) }) }) From 80c61919b9bd4cd02d2b18c8063f29a53191171b Mon Sep 17 00:00:00 2001 From: Your Name Date: Tue, 29 Nov 2022 15:02:21 -0800 Subject: [PATCH 4/8] fix test pt2 --- test/unit-tests/manualPricer.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/unit-tests/manualPricer.test.ts b/test/unit-tests/manualPricer.test.ts index 2e52a581c..da98b53dc 100644 --- a/test/unit-tests/manualPricer.test.ts +++ b/test/unit-tests/manualPricer.test.ts @@ -74,8 +74,8 @@ contract('ManualPricer', ([owner, bot, random]) => { const priceFromOracle = await oracle.getExpiryPrice(weth.address, expiryTimestamp) const lastExpiryTimestamp = await pricer.lastExpiryTimestamp() assert.equal(p1.toString(), priceFromOracle[0].toString()) - assert.equal(lastExpiryTimestamp, expiryTimestamp) - assert.equal(await pricer.historicalPrice(lastExpiryTimestamp), p1) + 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 () => { From cd8203e9d4358ad4c492510d64b7f720d5b032ed Mon Sep 17 00:00:00 2001 From: Your Name Date: Tue, 29 Nov 2022 15:25:40 -0800 Subject: [PATCH 5/8] fix test pt3 --- test/unit-tests/manualPricer.test.ts | 20 ++++++-------------- 1 file changed, 6 insertions(+), 14 deletions(-) diff --git a/test/unit-tests/manualPricer.test.ts b/test/unit-tests/manualPricer.test.ts index da98b53dc..d026396e4 100644 --- a/test/unit-tests/manualPricer.test.ts +++ b/test/unit-tests/manualPricer.test.ts @@ -35,7 +35,7 @@ contract('ManualPricer', ([owner, bot, random]) => { }) it('should revert if initializing oracle with 0 address', async () => { await expectRevert( - ManualPricer.new(bot, weth.address, oracle.address, ZERO_ADDR), + ManualPricer.new(bot, weth.address, oracle.address), 'ManualPricer: Cannot set 0 address as oracle', ) }) @@ -50,7 +50,7 @@ contract('ManualPricer', ([owner, bot, random]) => { describe('getPrice', () => { it('should return the new price after resetting answer', async () => { const newPrice = createTokenAmount(400, 8) - await pricer.setExpiryPriceInOracle(1, newPrice) + await pricer.setExpiryPriceInOracle(1, newPrice, { from: bot }) const price = await pricer.getPrice() const expectedResult = createTokenAmount(400, 8) assert.equal(price.toString(), expectedResult.toString()) @@ -58,17 +58,10 @@ contract('ManualPricer', ([owner, bot, random]) => { }) describe('setExpiryPrice', () => { - // time order: t0, t1, t2, t3, t4 - let t0: number, t1: number, t2: number, t3: number, t4: number - // p0 = price at t0 ... etc - const p0 = createTokenAmount(100, 8) const p1 = createTokenAmount(150.333, 8) - const p2 = createTokenAmount(180, 8) - const p3 = createTokenAmount(200, 8) - const p4 = createTokenAmount(140, 8) it('should set the correct price to the oracle', async () => { - const expiryTimestamp = (t0 + t1) / 2 // between t0 and t1 + const expiryTimestamp = 5 await pricer.setExpiryPriceInOracle(expiryTimestamp, p1, { from: bot }) const priceFromOracle = await oracle.getExpiryPrice(weth.address, expiryTimestamp) @@ -79,10 +72,9 @@ contract('ManualPricer', ([owner, bot, random]) => { }) it('should revert if sender is not bot address', async () => { - const expiryTimestamp = (t1 + t2) / 2 // between t0 and t1 - const roundId = 1 + const expiryTimestamp = 5 await expectRevert( - pricer.setExpiryPriceInOracle(expiryTimestamp, roundId, { from: random }), + pricer.setExpiryPriceInOracle(expiryTimestamp, p1, { from: random }), 'ManualPricer: unauthorized sender', ) }) @@ -102,7 +94,7 @@ contract('ManualPricer', ([owner, bot, random]) => { it('should revert when no data round available', async () => { const invalidRoundId = 2 - assert.equal(pricer.historicalPrice(2).toString(), '0', 'Historical round timestamp mismatch') + assert.equal((await pricer.historicalPrice(2)).toString(), '0', 'Historical round timestamp mismatch') }) }) }) From 0a6e4b1d99d4bc2509ab2eab3b7cc7764099e1e7 Mon Sep 17 00:00:00 2001 From: Your Name Date: Tue, 29 Nov 2022 15:48:02 -0800 Subject: [PATCH 6/8] fix test pt4 --- test/unit-tests/manualPricer.test.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/test/unit-tests/manualPricer.test.ts b/test/unit-tests/manualPricer.test.ts index d026396e4..24cf8e576 100644 --- a/test/unit-tests/manualPricer.test.ts +++ b/test/unit-tests/manualPricer.test.ts @@ -34,10 +34,7 @@ contract('ManualPricer', ([owner, bot, random]) => { assert.equal(oracleModule, oracle.address) }) it('should revert if initializing oracle with 0 address', async () => { - await expectRevert( - ManualPricer.new(bot, weth.address, oracle.address), - 'ManualPricer: Cannot set 0 address as oracle', - ) + 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( From 72fd46a71ff4829df1223cfb53e1c4288ed295a8 Mon Sep 17 00:00:00 2001 From: Your Name Date: Tue, 29 Nov 2022 15:55:22 -0800 Subject: [PATCH 7/8] fix deploy script --- scripts/deployManualPricer.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/deployManualPricer.js b/scripts/deployManualPricer.js index 19af98344..d2f87c9ea 100644 --- a/scripts/deployManualPricer.js +++ b/scripts/deployManualPricer.js @@ -17,7 +17,7 @@ module.exports = async function(callback) { console.log(`Deploying manual pricer contract on ${options.network} 🍕`) - const tx = await ManualPricer.new(options.bot, options.asset, options.aggregator, options.oracle, { + const tx = await ManualPricer.new(options.bot, options.asset, options.oracle, { gasPrice: options.gasPrice, gas: options.gasLimit, }) From 369b297ff210e89b81ef994bca9b89a118746ba0 Mon Sep 17 00:00:00 2001 From: Your Name Date: Wed, 30 Nov 2022 15:04:23 -0800 Subject: [PATCH 8/8] pass addr --- contracts/interfaces/OpynPricerInterface.sol | 2 +- contracts/pricers/ManualPricer.sol | 2 +- test/unit-tests/manualPricer.test.ts | 2 +- truffle-config.js | 13 +------------ 4 files changed, 4 insertions(+), 15 deletions(-) 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 index 8b3da9a9c..52808a90c 100644 --- a/contracts/pricers/ManualPricer.sol +++ b/contracts/pricers/ManualPricer.sol @@ -68,7 +68,7 @@ contract ManualPricer is OpynPricerInterface { * @dev overides the getPrice function in OpynPricerInterface * @return price of the asset in USD, scaled by 1e8 */ - function getPrice() external view override returns (uint256) { + function getPrice(address) external view override returns (uint256) { return historicalPrice[lastExpiryTimestamp]; } diff --git a/test/unit-tests/manualPricer.test.ts b/test/unit-tests/manualPricer.test.ts index 24cf8e576..9380c7138 100644 --- a/test/unit-tests/manualPricer.test.ts +++ b/test/unit-tests/manualPricer.test.ts @@ -48,7 +48,7 @@ contract('ManualPricer', ([owner, bot, random]) => { 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() + const price = await pricer.getPrice(oracle.address) const expectedResult = createTokenAmount(400, 8) assert.equal(price.toString(), expectedResult.toString()) }) 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: {