Skip to content
Draft
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
36 changes: 36 additions & 0 deletions contracts/contracts/interfaces/IOTokenVaultOracle.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.0;

import { AggregatorV3Interface } from "./chainlink/AggregatorV3Interface.sol";
import { IOToken } from "./IOToken.sol";
import { IVault } from "./IVault.sol";

/**
* @title OToken Vault Oracle Interface
* @author Origin Protocol Inc
*/
interface IOTokenVaultOracle is AggregatorV3Interface {
/**
* @notice Returns the Vault used to calculate the OToken price.
* @return The Vault contract.
*/
function vault() external view returns (IVault);

/**
* @notice Returns the OToken whose price is reported.
* @return The OToken contract.
*/
function oToken() external view returns (IOToken);

/**
* @notice Returns the value of one OToken in the Vault's underlying asset.
* @return The price with 18 decimals.
*/
function price() external view returns (uint256);

/**
* @notice Returns the latest price using the legacy Chainlink interface.
* @return The price with 18 decimals.
*/
function latestAnswer() external view returns (int256);
}
127 changes: 127 additions & 0 deletions contracts/contracts/oracle/OTokenVaultOracle.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.0;

import { IOToken } from "../interfaces/IOToken.sol";
import { IOTokenVaultOracle } from "../interfaces/IOTokenVaultOracle.sol";
import { IVault } from "../interfaces/IVault.sol";

/**
* @title OToken Vault Oracle
* @notice Reports the value of one OToken in the Vault's underlying asset.
* @dev The price has 18 decimals and is calculated from the Vault's total value
* divided by the OToken's total supply.
* @author Origin Protocol Inc
*/
contract OTokenVaultOracle is IOTokenVaultOracle {
/// @notice The number of decimals in the reported price.
uint8 public constant override decimals = 18;

/// @notice The Chainlink-compatible oracle version.
uint256 public constant override version = 1;

/// @notice The Vault used to calculate the OToken price.
IVault public immutable vault;

/// @notice The OToken whose price is reported.
IOToken public immutable oToken;

/// @notice A human-readable description of the price pair.
string public override description;

/**
* @notice Constructs an OToken Vault Oracle.
* @param _vault The Vault used to calculate the price and resolve the OToken.
* @param _description A human-readable description of the price pair.
*/
constructor(address _vault, string memory _description) {
require(_vault != address(0), "Vault is zero address");

vault = IVault(_vault);
address _oToken = vault.oToken();
require(_oToken != address(0), "OToken is zero address");
oToken = IOToken(_oToken);
description = _description;
}

/**
* @notice Returns the value of one OToken in the Vault's underlying asset.
* @return The price with 18 decimals.
*/
function price() public view override returns (uint256) {
uint256 supply = oToken.totalSupply();
require(supply > 0, "No data present");

return (vault.totalValue() * 1e18) / supply;
}

/**
* @notice Legacy Chainlink-compatible latest price accessor.
* @return The latest price with 18 decimals.
*/
function latestAnswer() external view override returns (int256) {
return int256(price());
}

/**
* @notice Returns data for the current synthetic round.
* @dev This is a live calculated feed, so only round 1 exists.
* @param _roundId The requested round ID, which must be 1.
* @return roundId The synthetic round ID.
* @return answer The current price with 18 decimals.
* @return startedAt The timestamp at which the price was read.
* @return updatedAt The timestamp at which the price was read.
* @return answeredInRound The synthetic round ID in which the answer was calculated.
*/
function getRoundData(uint80 _roundId)
external
view
override
returns (
uint80 roundId,
int256 answer,
uint256 startedAt,
uint256 updatedAt,
uint80 answeredInRound
)
{
require(_roundId == 1, "No data present");
return _latestRoundData();
}

/**
* @notice Returns the latest calculated price as Chainlink round data.
* @return roundId The synthetic round ID.
* @return answer The current price with 18 decimals.
* @return startedAt The timestamp at which the price was read.
* @return updatedAt The timestamp at which the price was read.
* @return answeredInRound The synthetic round ID in which the answer was calculated.
*/
function latestRoundData()
external
view
override
returns (
uint80 roundId,
int256 answer,
uint256 startedAt,
uint256 updatedAt,
uint80 answeredInRound
)
{
return _latestRoundData();
}

function _latestRoundData()
internal
view
returns (
uint80 roundId,
int256 answer,
uint256 startedAt,
uint256 updatedAt,
uint80 answeredInRound
)
{
return (1, int256(price()), block.timestamp, block.timestamp, 1);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.0;

import { OTokenVaultOracle } from "contracts/oracle/OTokenVaultOracle.sol";
import { AbstractDeployScript } from "scripts/deploy/helpers/AbstractDeployScript.s.sol";

/**
* @title 001_DeploySuperOETHVaultOracle
* @notice Deploys the superOETHb/WETH Vault Oracle on Base.
* @author Origin Protocol Inc
*/
contract $001_DeploySuperOETHVaultOracle is
AbstractDeployScript("001_DeploySuperOETHVaultOracle")
{
function _execute() internal override {
OTokenVaultOracle superOETHVaultOracle = new OTokenVaultOracle(
resolver.resolve("OETHBASE_VAULT_PROXY"),
"superOETHb / WETH"
);

_recordDeployment(
"OETHBASE_VAULT_ORACLE",
address(superOETHVaultOracle)
);
}

function _fork() internal override {
OTokenVaultOracle oracle = OTokenVaultOracle(
resolver.resolve("OETHBASE_VAULT_ORACLE")
);

require(
address(oracle.vault()) == resolver.resolve("OETHBASE_VAULT_PROXY"),
"Unexpected Vault"
);
require(
address(oracle.oToken()) == resolver.resolve("OETHBASE_PROXY"),
"Unexpected OToken"
);
require(oracle.decimals() == 18, "Unexpected decimals");
require(oracle.version() == 1, "Unexpected version");
require(
keccak256(bytes(oracle.description())) ==
keccak256(bytes("superOETHb / WETH")),
"Unexpected description"
);
require(oracle.price() > 0, "Invalid price");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.0;

import { OTokenVaultOracle } from "contracts/oracle/OTokenVaultOracle.sol";
import { AbstractDeployScript } from "scripts/deploy/helpers/AbstractDeployScript.s.sol";

/**
* @title 004_DeployOTokenVaultOracles
* @notice Deploys the OETH/WETH and OUSD/USDC Vault Oracles on mainnet.
* @author Origin Protocol Inc
*/
contract $004_DeployOTokenVaultOracles is
AbstractDeployScript("004_DeployOTokenVaultOracles")
{
function _execute() internal override {
OTokenVaultOracle oethVaultOracle = new OTokenVaultOracle(
resolver.resolve("OETH_VAULT_PROXY"),
"OETH / WETH"
);
OTokenVaultOracle ousdVaultOracle = new OTokenVaultOracle(
resolver.resolve("OUSD_VAULT_PROXY"),
"OUSD / USDC"
);

_recordDeployment("OETH_VAULT_ORACLE", address(oethVaultOracle));
_recordDeployment("OUSD_VAULT_ORACLE", address(ousdVaultOracle));
}

function _fork() internal override {
_verifyOracle(
resolver.resolve("OETH_VAULT_ORACLE"),
resolver.resolve("OETH_VAULT_PROXY"),
resolver.resolve("OETH_PROXY"),
"OETH / WETH"
);
_verifyOracle(
resolver.resolve("OUSD_VAULT_ORACLE"),
resolver.resolve("OUSD_VAULT_PROXY"),
resolver.resolve("OUSD_PROXY"),
"OUSD / USDC"
);
}

function _verifyOracle(
address oracleAddress,
address vaultAddress,
address oTokenAddress,
string memory expectedDescription
) internal view {
OTokenVaultOracle oracle = OTokenVaultOracle(oracleAddress);

require(address(oracle.vault()) == vaultAddress, "Unexpected Vault");
require(address(oracle.oToken()) == oTokenAddress, "Unexpected OToken");
require(oracle.decimals() == 18, "Unexpected decimals");
require(oracle.version() == 1, "Unexpected version");
require(
keccak256(bytes(oracle.description())) ==
keccak256(bytes(expectedDescription)),
"Unexpected description"
);
require(oracle.price() > 0, "Invalid price");
}
}
5 changes: 4 additions & 1 deletion contracts/tasks/lib/network.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { ethers } from "ethers";
import { resolveChain, getRpcEnvVar } from "@oplabs/talos-client";

/**
* Ambient network context for the standalone (hardhat-free) action runtime.
Expand Down Expand Up @@ -31,6 +30,10 @@ export function rpcUrlFor(nameOrId: string | number): {
networkName: string;
url: string;
} {
// Talos is an optional peer dependency and is only installed in the action
// runner image. Load it lazily so Hardhat can import shared utilities without
// requiring the private package.
const { resolveChain, getRpcEnvVar } = require("@oplabs/talos-client");
const chain = resolveChain(nameOrId);
let url: string | undefined;
if (process.env.FORK === "true" && process.env.LOCAL_PROVIDER_URL) {
Expand Down
23 changes: 23 additions & 0 deletions contracts/tests/mocks/MockOTokenVaultOracleDependencies.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.0;

contract MockOTokenVaultOracleVault {
uint256 public totalValue;
address public oToken;

function setTotalValue(uint256 _totalValue) external {
totalValue = _totalValue;
}

function setOToken(address _oToken) external {
oToken = _oToken;
}
}

contract MockOTokenVaultOracleToken {
uint256 public totalSupply;

function setTotalSupply(uint256 _totalSupply) external {
totalSupply = _totalSupply;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.0;

import { Unit_OTokenVaultOracle_Shared_Test } from "../shared/Shared.t.sol";

contract Unit_Concrete_OTokenVaultOracle_ViewFunctions_Test is
Unit_OTokenVaultOracle_Shared_Test
{
function test_constructor_setsConfiguration() public view {
assertEq(address(oracle.vault()), address(mockVault));
assertEq(address(oracle.oToken()), address(mockOToken));
assertEq(oracle.decimals(), 18);
assertEq(oracle.description(), "OToken / underlying asset");
assertEq(oracle.version(), 1);
}

function test_price_isOneAtRebase() public view {
assertEq(oracle.price(), 1e18);
}

function test_price_increasesWithUnrebasedYield() public {
mockVault.setTotalValue(105e18);
assertEq(oracle.price(), 1.05e18);
}

function test_price_returnsFractionalPrice() public {
mockVault.setTotalValue(101e18);
mockOToken.setTotalSupply(100e18);
assertEq(oracle.price(), 1.01e18);
}

function test_price_RevertWhen_supplyIsZero() public {
mockOToken.setTotalSupply(0);
vm.expectRevert("No data present");
oracle.price();
}

function test_latestAnswer_returnsPrice() public view {
assertEq(oracle.latestAnswer(), int256(1e18));
}

function test_latestRoundData_returnsCurrentPrice() public view {
(
uint80 roundId,
int256 answer,
uint256 startedAt,
uint256 updatedAt,
uint80 answeredInRound
) = oracle.latestRoundData();

assertEq(roundId, 1);
assertEq(answer, int256(1e18));
assertEq(startedAt, block.timestamp);
assertEq(updatedAt, block.timestamp);
assertEq(answeredInRound, 1);
}

function test_getRoundData_returnsCurrentPriceForSyntheticRound()
public
view
{
(uint80 roundId, int256 answer, , , uint80 answeredInRound) = oracle
.getRoundData(1);

assertEq(roundId, 1);
assertEq(answer, int256(1e18));
assertEq(answeredInRound, 1);
}

function test_getRoundData_RevertWhen_roundDoesNotExist() public {
vm.expectRevert("No data present");
oracle.getRoundData(2);
}
}
Loading
Loading