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

feat/savax-pricer Adding sAVAX pricer #454

Open
wants to merge 3 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/SAVAXInterface.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
// SPDX-License-Identifier: MIT
pragma solidity =0.6.10;

interface SAVAXInterface {
function getSharesByPooledAvax(uint256 avaxAmount) external view returns (uint256);

function getPooledAvaxByShares(uint256 shareAmount) external view returns (uint256);

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

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

contract MockSAVAXToken is ERC20Upgradeable {
uint256 public actualAvaxDepositAmount;

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 setPooledAvaxByShares(uint256 _actualAvaxDepositAmount) external {
actualAvaxDepositAmount = _actualAvaxDepositAmount;
}

// solhint-disable-next-line
function getPooledAvaxByShares(uint256 _shareAmount) external view returns (uint256) {
return actualAvaxDepositAmount;
}
}
91 changes: 91 additions & 0 deletions contracts/pricers/SAvaxPricer.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity =0.6.10;
import {OracleInterface} from "../interfaces/OracleInterface.sol";
import {OpynPricerInterface} from "../interfaces/OpynPricerInterface.sol";
import {SAVAXInterface} from "../interfaces/SAVAXInterface.sol";
import {SafeMath} from "../packages/oz/SafeMath.sol";

/**
* Error Codes
* W1: cannot deploy pricer, sAVAX 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 SAvaxPricer
* @author Ben Burns ([email protected])
* @notice A Pricer contract for a sAVAX token
*/
contract SAvaxPricer is OpynPricerInterface {
using SafeMath for uint256;

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

/// @notice sAVAX token
SAVAXInterface public sAVAX;

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

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

sAVAX = SAVAXInterface(_sAVAX);
oracle = OracleInterface(_oracle);
underlying = _underlying;
}

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

/**
* @notice set the expiry price in the oracle
* @dev requires that the underlying price has been set before setting a sAVAx 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 sAvaxPrice = _underlyingPriceToSAvaxPrice(underlyingPriceExpiry);
oracle.setExpiryPrice(address(sAVAX), _expiryTimestamp, sAvaxPrice);
}

/**
* @dev convert underlying price to sAVAX price with the sAVAX to AVAX exchange rate (1 sAVAX ≈ 1 AVAX)
* @param _underlyingPrice price of 1 underlying token (ie 1e18 AVAX) in USD, scaled by 1e8
* @return price of 1 sAVAX in USD, scaled by 1e8
*/
function _underlyingPriceToSAvaxPrice(uint256 _underlyingPrice) private view returns (uint256) {
// Passing 1e18 to getPooledAvaxByShares() gives us the number of AVAX per sAVAX.
uint256 sAvaxPerAvax = sAVAX.getPooledAvaxByShares(1e18);
return sAvaxPerAvax.mul(_underlyingPrice).div(1e18);
}

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

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

module.exports = async function (callback) {
try {
const options = yargs
.usage(
'Usage: --network <network> --sAvax <sAvax> --underlying <underlying> --oracle <oracle> --gasPrice <gasPrice> --gasLimit <gasLimit>',
)
.option('network', { describe: 'Network name', type: 'string', demandOption: true })
.option('sAvax', { describe: 'sAvax 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 sAvax pricer contract to ${options.network} 🍕`)

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

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

callback()
} catch (err) {
callback(err)
}
}
113 changes: 113 additions & 0 deletions test/unit-tests/sAvaxPricer.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
import {
MockPricerInstance,
MockOracleInstance,
MockERC20Instance,
MockSAVAXTokenInstance,
SAvaxPricerInstance,
} 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 MockSAVAXToken = artifacts.require('MockSAVAXToken.sol')
const SAvaxPricer = artifacts.require('SAvaxPricer.sol')

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

contract('SAvaxPricer', ([owner, random]) => {
let oracle: MockOracleInstance
let wAvax: MockERC20Instance
let wAvaxPricer: MockPricerInstance
let sAvax: MockSAVAXTokenInstance
let sAvaxPricer: SAvaxPricerInstance

before('Deployment', async () => {
oracle = await MockOracle.new({ from: owner })
wAvax = await MockERC20.new('WAVAX', 'WAVAX', 18)
sAvax = await MockSAVAXToken.new('sAVAX', 'sAVAX')

wAvaxPricer = await MockPricer.new(wAvax.address, oracle.address)
sAvaxPricer = await SAvaxPricer.new(sAvax.address, wAvax.address, oracle.address)

await oracle.setAssetPricer(wAvax.address, wAvaxPricer.address)
})

describe('constructor', () => {
it('should deploy the contract successfully with correct values', async () => {
assert.equal(await sAvaxPricer.sAVAX(), sAvax.address)
assert.equal(await sAvaxPricer.underlying(), wAvax.address)
assert.equal(await sAvaxPricer.oracle(), oracle.address)
})

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

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

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

describe('getPrice for sAvax', () => {
const avaxPrice = createScaledNumber(89.78)
const pricePerShare = new BigNumber('1002001490131578947')
before('mock data in chainlink pricer and sAvax', async () => {
await oracle.setRealTimePrice(wAvax.address, avaxPrice)
await sAvax.setPooledAvaxByShares(pricePerShare)
})
it('should return the price in 1e8', async () => {
const sAvaxprice = await sAvaxPricer.getPrice()
const expectResult = await underlyingPriceToYTokenPrice(new BigNumber(avaxPrice), pricePerShare, wAvax)
assert.equal(sAvaxprice.toString(), expectResult.toString())
// hardcoded answer, 1 sAVAX = 1.00200149 AVAX
assert.equal(sAvaxprice.toString(), '8995969378')
})

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

it('should revert if price is lower than 0', async () => {
await oracle.setRealTimePrice(wAvax.address, '0')
await expectRevert(sAvaxPricer.getPrice(), 'W4')
})
})

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

before('setup oracle record for wAvax 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(sAvaxPricer.setExpiryPriceInOracle(expiry), 'W5')
})

it('should set price successfully by arbitrary address', async () => {
await oracle.setExpiryPrice(wAvax.address, expiry, avaxPrice)
await sAvaxPricer.setExpiryPriceInOracle(expiry, { from: random })
const [price] = await oracle.getExpiryPrice(sAvax.address, expiry)
const expectedResult = await underlyingPriceToYTokenPrice(avaxPrice, pricePerShare, wAvax)
assert.equal(price.toString(), expectedResult.toString())
})
})
})
4 changes: 2 additions & 2 deletions truffle-config.js
Original file line number Diff line number Diff line change
Expand Up @@ -64,11 +64,11 @@ module.exports = {
},
avax: {
provider: () => new HDWalletProvider(mnemonic, 'https://api.avax.network/ext/bc/C/rpc'),
network_id: 1,
network_id: 43114,
},
fuji: {
provider: () => new HDWalletProvider(mnemonic, 'https://api.avax-test.network/ext/bc/C/rpc'),
network_id: 1,
network_id: 43113,
gas: 8000000,
gasPrice: 250000000000,
skipDryRun: true,
Expand Down