From c4a78e983c57f5f3061aaf867e728653f23b4abb Mon Sep 17 00:00:00 2001 From: mario Date: Wed, 31 Mar 2021 17:36:00 -0700 Subject: [PATCH 1/3] port ctoken oracle --- contracts/interfaces/external/ICToken.sol | 36 ++++ .../integration/oracles/CTokenOracle.sol | 99 +++++++++++ test/integration/oracles/cTokenOracle.spec.ts | 155 ++++++++++++++++++ utils/contracts/index.ts | 1 + utils/deploys/deployOracles.ts | 27 +++ utils/deploys/index.ts | 5 +- 6 files changed, 321 insertions(+), 2 deletions(-) create mode 100644 contracts/interfaces/external/ICToken.sol create mode 100644 contracts/protocol/integration/oracles/CTokenOracle.sol create mode 100644 test/integration/oracles/cTokenOracle.spec.ts create mode 100644 utils/deploys/deployOracles.ts diff --git a/contracts/interfaces/external/ICToken.sol b/contracts/interfaces/external/ICToken.sol new file mode 100644 index 000000000..3ae724c35 --- /dev/null +++ b/contracts/interfaces/external/ICToken.sol @@ -0,0 +1,36 @@ +/* + 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; + + +/** + * @title ICToken + * @author Set Protocol + * + * Interface for interacting with Compound cTokens + */ +interface ICToken { + + /** + * Calculates the exchange rate from the underlying to the CToken + * + * @notice Accrue interest then return the up-to-date exchange rate + * @return Calculated exchange rate scaled by 1e18 + */ + function exchangeRateCurrent() external returns (uint256); + + function exchangeRateStored() external view returns (uint256); + + function decimals() external view returns(uint8); +} diff --git a/contracts/protocol/integration/oracles/CTokenOracle.sol b/contracts/protocol/integration/oracles/CTokenOracle.sol new file mode 100644 index 000000000..1687faa4f --- /dev/null +++ b/contracts/protocol/integration/oracles/CTokenOracle.sol @@ -0,0 +1,99 @@ +/* + 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 { ICToken } from "../../../interfaces/external/ICToken.sol"; +import { IOracle } from "../../../interfaces/IOracle.sol"; + + +/** + * @title CTokenOracle + * @author Set Protocol + * + * Oracle built to retrieve the cToken price + */ +contract CTokenOracle is IOracle +{ + using SafeMath for uint256; + + /* ============ State Variables ============ */ + ICToken public cToken; + IOracle public underlyingOracle; // Underlying token oracle + string public dataDescription; + + // Exchange Rate values are scaled by 1e18 + uint256 internal constant scalingFactor = 10 ** 18; + + // CToken Full Unit + uint256 public cTokenFullUnit; + + // Underlying Asset Full Unit + uint256 public 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( + ICToken _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 + .mul(conversionRate) + .mul(cTokenFullUnit) + .div(underlyingFullUnit) + .div(scalingFactor); + } +} 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); } } - - From 0b3cfaf442f9652f410aa789a4b9960e85c1b8d5 Mon Sep 17 00:00:00 2001 From: mario Date: Mon, 5 Apr 2021 16:17:11 -0700 Subject: [PATCH 2/3] fix PR comments --- .../integration/oracles/CTokenOracle.sol | 17 +++++------------ 1 file changed, 5 insertions(+), 12 deletions(-) diff --git a/contracts/protocol/integration/oracles/CTokenOracle.sol b/contracts/protocol/integration/oracles/CTokenOracle.sol index 1687faa4f..3dab7c411 100644 --- a/contracts/protocol/integration/oracles/CTokenOracle.sol +++ b/contracts/protocol/integration/oracles/CTokenOracle.sol @@ -15,6 +15,7 @@ pragma solidity 0.6.10; import { SafeMath } from "@openzeppelin/contracts/math/SafeMath.sol"; +import { PreciseUnitMath } from "../../../lib/PreciseUnitMath.sol"; import { ICToken } from "../../../interfaces/external/ICToken.sol"; import { IOracle } from "../../../interfaces/IOracle.sol"; @@ -23,20 +24,17 @@ import { IOracle } from "../../../interfaces/IOracle.sol"; * @title CTokenOracle * @author Set Protocol * - * Oracle built to retrieve the cToken price + * Oracle built to return cToken price by multiplying the underlying asset price by Compound's stored exchange rate */ -contract CTokenOracle is IOracle -{ +contract CTokenOracle is IOracle { using SafeMath for uint256; + using PreciseUnitMath for uint256; /* ============ State Variables ============ */ ICToken public cToken; IOracle public underlyingOracle; // Underlying token oracle string public dataDescription; - // Exchange Rate values are scaled by 1e18 - uint256 internal constant scalingFactor = 10 ** 18; - // CToken Full Unit uint256 public cTokenFullUnit; @@ -70,7 +68,6 @@ contract CTokenOracle is IOracle /** * 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 @@ -90,10 +87,6 @@ contract CTokenOracle is IOracle // Price of underlying is the price value / Token * conversion / scaling factor // Values need to be converted based on full unit quantities - return underlyingPrice - .mul(conversionRate) - .mul(cTokenFullUnit) - .div(underlyingFullUnit) - .div(scalingFactor); + return underlyingPrice.preciseMul(conversionRate).mul(cTokenFullUnit).div(underlyingFullUnit); } } From a80da2fd14282349aabf755119b2c9335a09d98f Mon Sep 17 00:00:00 2001 From: mario Date: Fri, 9 Apr 2021 15:41:28 -0700 Subject: [PATCH 3/3] use icerc20 interface --- contracts/interfaces/external/ICToken.sol | 36 ------------------- .../integration/oracles/CTokenOracle.sol | 12 +++---- 2 files changed, 6 insertions(+), 42 deletions(-) delete mode 100644 contracts/interfaces/external/ICToken.sol diff --git a/contracts/interfaces/external/ICToken.sol b/contracts/interfaces/external/ICToken.sol deleted file mode 100644 index 3ae724c35..000000000 --- a/contracts/interfaces/external/ICToken.sol +++ /dev/null @@ -1,36 +0,0 @@ -/* - 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; - - -/** - * @title ICToken - * @author Set Protocol - * - * Interface for interacting with Compound cTokens - */ -interface ICToken { - - /** - * Calculates the exchange rate from the underlying to the CToken - * - * @notice Accrue interest then return the up-to-date exchange rate - * @return Calculated exchange rate scaled by 1e18 - */ - function exchangeRateCurrent() external returns (uint256); - - function exchangeRateStored() external view returns (uint256); - - function decimals() external view returns(uint8); -} diff --git a/contracts/protocol/integration/oracles/CTokenOracle.sol b/contracts/protocol/integration/oracles/CTokenOracle.sol index 3dab7c411..d6ba5c860 100644 --- a/contracts/protocol/integration/oracles/CTokenOracle.sol +++ b/contracts/protocol/integration/oracles/CTokenOracle.sol @@ -16,7 +16,7 @@ pragma solidity 0.6.10; import { SafeMath } from "@openzeppelin/contracts/math/SafeMath.sol"; import { PreciseUnitMath } from "../../../lib/PreciseUnitMath.sol"; -import { ICToken } from "../../../interfaces/external/ICToken.sol"; +import { ICErc20 } from "../../../interfaces/external/ICErc20.sol"; import { IOracle } from "../../../interfaces/IOracle.sol"; @@ -31,15 +31,15 @@ contract CTokenOracle is IOracle { using PreciseUnitMath for uint256; /* ============ State Variables ============ */ - ICToken public cToken; - IOracle public underlyingOracle; // Underlying token oracle + ICErc20 public immutable cToken; + IOracle public immutable underlyingOracle; // Underlying token oracle string public dataDescription; // CToken Full Unit - uint256 public cTokenFullUnit; + uint256 public immutable cTokenFullUnit; // Underlying Asset Full Unit - uint256 public underlyingFullUnit; + uint256 public immutable underlyingFullUnit; /* ============ Constructor ============ */ @@ -51,7 +51,7 @@ contract CTokenOracle is IOracle { * @param _dataDescription Human readable description of oracle */ constructor( - ICToken _cToken, + ICErc20 _cToken, IOracle _underlyingOracle, uint256 _cTokenFullUnit, uint256 _underlyingFullUnit,