diff --git a/contracts/protocol/integration/oracles/CTokenOracle.sol b/contracts/protocol/integration/oracles/CTokenOracle.sol new file mode 100644 index 000000000..d6ba5c860 --- /dev/null +++ b/contracts/protocol/integration/oracles/CTokenOracle.sol @@ -0,0 +1,92 @@ +/* + Copyright 2021 Set Labs Inc. + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +pragma solidity 0.6.10; + +import { SafeMath } from "@openzeppelin/contracts/math/SafeMath.sol"; + +import { PreciseUnitMath } from "../../../lib/PreciseUnitMath.sol"; +import { ICErc20 } from "../../../interfaces/external/ICErc20.sol"; +import { IOracle } from "../../../interfaces/IOracle.sol"; + + +/** + * @title CTokenOracle + * @author Set Protocol + * + * Oracle built to return cToken price by multiplying the underlying asset price by Compound's stored exchange rate + */ +contract CTokenOracle is IOracle { + using SafeMath for uint256; + using PreciseUnitMath for uint256; + + /* ============ State Variables ============ */ + ICErc20 public immutable cToken; + IOracle public immutable underlyingOracle; // Underlying token oracle + string public dataDescription; + + // CToken Full Unit + uint256 public immutable cTokenFullUnit; + + // Underlying Asset Full Unit + uint256 public immutable underlyingFullUnit; + + /* ============ Constructor ============ */ + + /* + * @param _cToken The address of Compound Token + * @param _underlyingOracle The address of the underlying oracle + * @param _cTokenFullUnit The full unit of the Compound Token + * @param _underlyingFullUnit The full unit of the underlying asset + * @param _dataDescription Human readable description of oracle + */ + constructor( + ICErc20 _cToken, + IOracle _underlyingOracle, + uint256 _cTokenFullUnit, + uint256 _underlyingFullUnit, + string memory _dataDescription + ) + public + { + cToken = _cToken; + cTokenFullUnit = _cTokenFullUnit; + underlyingFullUnit = _underlyingFullUnit; + underlyingOracle = _underlyingOracle; + dataDescription = _dataDescription; + } + + /** + * Returns the price value of a full cToken denominated in underlyingOracle value + * The underlying oracle is assumed to return a price of 18 decimal + * for a single full token of the underlying asset. The derived price + * of the cToken is then the price of a unit of underlying multiplied + * by the exchangeRate, adjusted for decimal differences, and descaled. + */ + function read() + external + override + view + returns (uint256) + { + // Retrieve the price of the underlying + uint256 underlyingPrice = underlyingOracle.read(); + + // Retrieve cToken underlying to cToken stored conversion rate + uint256 conversionRate = cToken.exchangeRateStored(); + + // Price of underlying is the price value / Token * conversion / scaling factor + // Values need to be converted based on full unit quantities + return underlyingPrice.preciseMul(conversionRate).mul(cTokenFullUnit).div(underlyingFullUnit); + } +} diff --git a/test/integration/oracles/cTokenOracle.spec.ts b/test/integration/oracles/cTokenOracle.spec.ts new file mode 100644 index 000000000..3a1b07d91 --- /dev/null +++ b/test/integration/oracles/cTokenOracle.spec.ts @@ -0,0 +1,155 @@ +import "module-alias/register"; +import { BigNumber } from "@ethersproject/bignumber"; + +import { Address } from "@utils/types"; +import { Account } from "@utils/test/types"; +import { CERc20 } from "@utils/contracts/compound"; +import { OracleMock, CTokenOracle } from "@utils/contracts"; +import DeployHelper from "@utils/deploys"; + +import { + ether +} from "@utils/index"; +import { + getAccounts, + getWaffleExpect, + getSystemFixture, + getCompoundFixture, + addSnapshotBeforeRestoreAfterEach, +} from "@utils/test/index"; +import { CompoundFixture, SystemFixture } from "@utils/fixtures"; + +const expect = getWaffleExpect(); + +describe("CTokenOracle", () => { + let owner: Account; + let deployer: DeployHelper; + let setup: SystemFixture; + + let compoundSetup: CompoundFixture; + let cDai: CERc20; + let exchangeRate: BigNumber; + let daiUsdcOracle: OracleMock; + let cDaiOracle: CTokenOracle; + let cDaiFullUnit: BigNumber; + let daiFullUnit: BigNumber; + + before(async () => { + [ + owner, + ] = await getAccounts(); + + // System setup + deployer = new DeployHelper(owner.wallet); + setup = getSystemFixture(owner.address); + await setup.initialize(); + + // Compound setup + compoundSetup = getCompoundFixture(owner.address); + await compoundSetup.initialize(); + + exchangeRate = ether(0.5); + + cDai = await compoundSetup.createAndEnableCToken( + setup.dai.address, + exchangeRate, + compoundSetup.comptroller.address, + compoundSetup.interestRateModel.address, + "Compound DAI", + "cDAI", + 8, + ether(0.75), // 75% collateral factor + ether(1) + ); + + daiUsdcOracle = await deployer.mocks.deployOracleMock(ether(1)); + cDaiFullUnit = BigNumber.from("100000000"); + daiFullUnit = BigNumber.from("1000000000000000000"); + cDaiOracle = await deployer.oracles.deployCTokenOracle( + cDai.address, + daiUsdcOracle.address, + cDaiFullUnit, + daiFullUnit, + "cDAI Oracle" + ); + + }); + + addSnapshotBeforeRestoreAfterEach(); + + describe("#constructor", async () => { + let subjectCToken: Address; + let subjectUnderlyingOracle: Address; + let subjectCTokenFullUnit: BigNumber; + let subjectUnderlyingFullUnit: BigNumber; + let subjectDataDescription: string; + + before(async () => { + subjectCToken = cDai.address; + subjectCTokenFullUnit = BigNumber.from("100000000"); + subjectUnderlyingFullUnit = BigNumber.from("1000000000000000000"); + subjectUnderlyingOracle = daiUsdcOracle.address; + subjectDataDescription = "cDAI Oracle"; + }); + + async function subject(): Promise { + return deployer.oracles.deployCTokenOracle( + subjectCToken, + subjectUnderlyingOracle, + subjectCTokenFullUnit, + subjectUnderlyingFullUnit, + subjectDataDescription + ); + } + + it("sets the correct cToken address", async () => { + const cTokenOracle = await subject(); + const cTokenAddress = await cTokenOracle.cToken(); + expect(cTokenAddress).to.equal(subjectCToken); + }); + + it("sets the correct cToken full unit", async () => { + const cTokenOracle = await subject(); + const cTokenFullUnit = await cTokenOracle.cTokenFullUnit(); + expect(cTokenFullUnit).to.eq(subjectCTokenFullUnit); + }); + + it("sets the correct underlying full unit", async () => { + const cTokenOracle = await subject(); + const underlyingFullUnit = await cTokenOracle.underlyingFullUnit(); + expect(underlyingFullUnit).to.eq(subjectUnderlyingFullUnit); + }); + + it("sets the correct underlying oracle address", async () => { + const cTokenOracle = await subject(); + const underlyingOracleAddress = await cTokenOracle.underlyingOracle(); + expect(underlyingOracleAddress).to.eq(subjectUnderlyingOracle); + }); + + it("sets the correct data description", async () => { + const cTokenOracle = await subject(); + const actualDataDescription = await cTokenOracle.dataDescription(); + expect(actualDataDescription).to.eq(subjectDataDescription); + }); + + }); + + + describe("#read", async () => { + + async function subject(): Promise { + return cDaiOracle.read(); + } + + it("returns the correct cTokenValue", async () => { + const result = await subject(); + const expectedResult = ether(1) + .mul(exchangeRate) + .mul(cDaiFullUnit) + .div(daiFullUnit) + .div(ether(1)); + + expect(result).to.eq(expectedResult); + }); + }); +}); diff --git a/utils/contracts/index.ts b/utils/contracts/index.ts index dc42b7feb..9ec8b7de3 100644 --- a/utils/contracts/index.ts +++ b/utils/contracts/index.ts @@ -49,6 +49,7 @@ export { OneInchExchangeAdapter } from "../../typechain/OneInchExchangeAdapter"; export { OneInchExchangeMock } from "../../typechain/OneInchExchangeMock"; export { OracleAdapterMock } from "../../typechain/OracleAdapterMock"; export { OracleMock } from "../../typechain/OracleMock"; +export { CTokenOracle } from "../../typechain/CTokenOracle"; export { PositionMock } from "../../typechain/PositionMock"; export { PreciseUnitMathMock } from "../../typechain/PreciseUnitMathMock"; export { PriceOracle } from "../../typechain/PriceOracle"; diff --git a/utils/deploys/deployOracles.ts b/utils/deploys/deployOracles.ts new file mode 100644 index 000000000..e049d3652 --- /dev/null +++ b/utils/deploys/deployOracles.ts @@ -0,0 +1,27 @@ +import { Signer } from "ethers"; +import { Address } from "../types"; +import { BigNumber } from "@ethersproject/bignumber"; + +import { + CTokenOracle, +} from "../contracts"; + +import { CTokenOracle__factory } from "../../typechain/factories/CTokenOracle__factory"; + +export default class DeployOracles { + private _deployerSigner: Signer; + + constructor(deployerSigner: Signer) { + this._deployerSigner = deployerSigner; + } + + public async deployCTokenOracle( + cToken: Address, + underlyingOracle: Address, + cTokenFullUnit: BigNumber, + underlyingFullUnit: BigNumber, + dataDescription: string): Promise { + return await new CTokenOracle__factory(this._deployerSigner) + .deploy(cToken, underlyingOracle, cTokenFullUnit, underlyingFullUnit, dataDescription); + } +} diff --git a/utils/deploys/index.ts b/utils/deploys/index.ts index fb2ac5931..647a9a72f 100644 --- a/utils/deploys/index.ts +++ b/utils/deploys/index.ts @@ -8,6 +8,7 @@ import DeployExternalContracts from "./deployExternal"; import DeployAdapters from "./deployAdapters"; import DeployViewers from "./deployViewers"; import DeployProduct from "./deployProduct"; +import DeployOracles from "./deployOracles"; export default class DeployHelper { public libraries: DeployLibraries; @@ -18,6 +19,7 @@ export default class DeployHelper { public adapters: DeployAdapters; public viewers: DeployViewers; public product: DeployProduct; + public oracles: DeployOracles; constructor(deployerSigner: Signer) { this.libraries = new DeployLibraries(deployerSigner); @@ -28,7 +30,6 @@ export default class DeployHelper { this.adapters = new DeployAdapters(deployerSigner); this.viewers = new DeployViewers(deployerSigner); this.product = new DeployProduct(deployerSigner); + this.oracles = new DeployOracles(deployerSigner); } } - -