diff --git a/contracts/PriceOracleProxy.sol b/contracts/PriceOracleProxy.sol index a506673e6..a562bf863 100644 --- a/contracts/PriceOracleProxy.sol +++ b/contracts/PriceOracleProxy.sol @@ -4,60 +4,83 @@ import "./CErc20.sol"; import "./CToken.sol"; import "./PriceOracle.sol"; import "./Comptroller.sol"; +import "./SafeMath.sol"; interface V1PriceOracleInterface { function assetPrices(address asset) external view returns (uint); } contract PriceOracleProxy is PriceOracle { + using SafeMath for uint256; + /** - * @notice The v1 price oracle, which will continue to serve prices - * prices for v1 assets + * @notice The v1 price oracle, which will continue to serve prices for v1 assets */ V1PriceOracleInterface public v1PriceOracle; /** - * @notice The active comptroller, which will be checked for listing status - * to short circuit and return 0 for unlisted assets. - * - * @dev Listed markets are not part of the comptroller interface used by - * cTokens, so we assumena an instance of v1 comptroller.sol instead + * @notice The comptroller which is used to white-list assets the proxy will price + * @dev Assets which are not white-listed will not be priced, to defend against abuse */ Comptroller public comptroller; /** * @notice address of the cEther contract, which has a constant price */ - address public cEtherAddress; + address public cEthAddress; /** * @notice address of the cUSDC contract, which we hand pick a key for */ address public cUsdcAddress; + /** + * @notice address of the cDAI contract, which we hand pick a key for + */ + address public cDaiAddress; + /** * @notice address of the USDC contract, which we hand pick a key for */ address constant usdcOracleKey = address(1); + /** + * @notice address of the DAI contract, which we hand pick a key for + */ + address constant daiOracleKey = address(2); + + /** + * @notice address of the asset which contains the USD/ETH price from Maker + */ + address public makerUsdOracleKey; + /** * @notice Indicator that this is a PriceOracle contract (for inspection) */ bool public constant isPriceOracle = true; /** - * @param comptroller_ The address of the active comptroller, which will - * be consulted for market listing status. - * @param v1PriceOracle_ The address of the v1 price oracle, which will - * continue to operate and hold prices for collateral assets. - * @param cEtherAddress_ The address of the cEther contract, which will - * return a constant 1e18, since all prices relative to ether + * @param comptroller_ The address of the comptroller, which will be consulted for market listing status + * @param v1PriceOracle_ The address of the v1 price oracle, which will continue to operate and hold prices for collateral assets + * @param cEthAddress_ The address of cETH, which will return a constant 1e18, since all prices relative to ether + * @param cUsdcAddress_ The address of cUSDC, which will be read from a special oracle key + * @param cDaiAddress_ The address of cDAI, which will be read from a special oracle key */ - constructor(address comptroller_, address v1PriceOracle_, address cEtherAddress_, address cUsdcAddress_) public { + constructor(address comptroller_, + address v1PriceOracle_, + address cEthAddress_, + address cUsdcAddress_, + address cDaiAddress_) public { comptroller = Comptroller(comptroller_); v1PriceOracle = V1PriceOracleInterface(v1PriceOracle_); - cEtherAddress = cEtherAddress_; + + cEthAddress = cEthAddress_; cUsdcAddress = cUsdcAddress_; + cDaiAddress = cDaiAddress_; + + if (cDaiAddress_ != address(0)) { + makerUsdOracleKey = CErc20(cDaiAddress_).underlying(); + } } /** @@ -71,18 +94,42 @@ contract PriceOracleProxy is PriceOracle { (bool isListed, ) = comptroller.markets(cTokenAddress); if (!isListed) { - // not listed, worthless + // not white-listed, worthless return 0; - } else if (cTokenAddress == cEtherAddress) { + } + + if (cTokenAddress == cEthAddress) { // ether always worth 1 return 1e18; - } else if (cTokenAddress == cUsdcAddress) { - // read from hand picked key - return v1PriceOracle.assetPrices(usdcOracleKey); - } else { - // read from v1 oracle - address underlying = CErc20(cTokenAddress).underlying(); - return v1PriceOracle.assetPrices(underlying); } + + if (cTokenAddress == cUsdcAddress) { + // we assume USDC/USD = 1, and let DAI/ETH float based on the DAI/USDC ratio + // use the maker usd price (for a token w/ 6 decimals) + return v1PriceOracle.assetPrices(makerUsdOracleKey).mul(1e12); // 1e(18 - 6) + } + + if (cTokenAddress == cDaiAddress) { + // check and bound the DAI/USDC posted price ratio + // and use that to scale the maker price (for a token w/ 18 decimals) + uint makerUsdPrice = v1PriceOracle.assetPrices(makerUsdOracleKey); + uint postedUsdcPrice = v1PriceOracle.assetPrices(usdcOracleKey); + uint postedScaledDaiPrice = v1PriceOracle.assetPrices(daiOracleKey).mul(1e12); + uint daiUsdcRatio = postedScaledDaiPrice.mul(1e18).div(postedUsdcPrice); + + if (daiUsdcRatio < 0.95e18) { + return makerUsdPrice.mul(0.95e18).div(1e18); + } + + if (daiUsdcRatio > 1.05e18) { + return makerUsdPrice.mul(1.05e18).div(1e18); + } + + return makerUsdPrice.mul(daiUsdcRatio).div(1e18); + } + + // otherwise just read from v1 oracle + address underlying = CErc20(cTokenAddress).underlying(); + return v1PriceOracle.assetPrices(underlying); } } diff --git a/contracts/SafeMath.sol b/contracts/SafeMath.sol new file mode 100644 index 000000000..1217f8ff9 --- /dev/null +++ b/contracts/SafeMath.sol @@ -0,0 +1,161 @@ +pragma solidity ^0.5.8; + +// From https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/math/Math.sol +// Subject to the MIT license. + +/** + * @dev Wrappers over Solidity's arithmetic operations with added overflow + * checks. + * + * Arithmetic operations in Solidity wrap on overflow. This can easily result + * in bugs, because programmers usually assume that an overflow raises an + * error, which is the standard behavior in high level programming languages. + * `SafeMath` restores this intuition by reverting the transaction when an + * operation overflows. + * + * Using this library instead of the unchecked operations eliminates an entire + * class of bugs, so it's recommended to use it always. + */ +library SafeMath { + /** + * @dev Returns the addition of two unsigned integers, reverting on + * overflow. + * + * Counterpart to Solidity's `+` operator. + * + * Requirements: + * - Addition cannot overflow. + */ + function add(uint256 a, uint256 b) internal pure returns (uint256) { + uint256 c = a + b; + require(c >= a, "SafeMath: addition overflow"); + + return c; + } + + /** + * @dev Returns the subtraction of two unsigned integers, reverting on + * overflow (when the result is negative). + * + * Counterpart to Solidity's `-` operator. + * + * Requirements: + * - Subtraction cannot overflow. + */ + function sub(uint256 a, uint256 b) internal pure returns (uint256) { + return sub(a, b, "SafeMath: subtraction overflow"); + } + + /** + * @dev Returns the subtraction of two unsigned integers, reverting with custom message on + * overflow (when the result is negative). + * + * Counterpart to Solidity's `-` operator. + * + * Requirements: + * - Subtraction cannot overflow. + * + * NOTE: This is a feature of the next version of OpenZeppelin Contracts. + * @dev Get it via `npm install @openzeppelin/contracts@next`. + */ + function sub(uint256 a, uint256 b, string memory errorMessage) internal pure returns (uint256) { + require(b <= a, errorMessage); + uint256 c = a - b; + + return c; + } + + /** + * @dev Returns the multiplication of two unsigned integers, reverting on + * overflow. + * + * Counterpart to Solidity's `*` operator. + * + * Requirements: + * - Multiplication cannot overflow. + */ + function mul(uint256 a, uint256 b) internal pure returns (uint256) { + // Gas optimization: this is cheaper than requiring 'a' not being zero, but the + // benefit is lost if 'b' is also tested. + // See: https://github.com/OpenZeppelin/openzeppelin-contracts/pull/522 + if (a == 0) { + return 0; + } + + uint256 c = a * b; + require(c / a == b, "SafeMath: multiplication overflow"); + + return c; + } + + /** + * @dev Returns the integer division of two unsigned integers. Reverts on + * division by zero. The result is rounded towards zero. + * + * Counterpart to Solidity's `/` operator. Note: this function uses a + * `revert` opcode (which leaves remaining gas untouched) while Solidity + * uses an invalid opcode to revert (consuming all remaining gas). + * + * Requirements: + * - The divisor cannot be zero. + */ + function div(uint256 a, uint256 b) internal pure returns (uint256) { + return div(a, b, "SafeMath: division by zero"); + } + + /** + * @dev Returns the integer division of two unsigned integers. Reverts with custom message on + * division by zero. The result is rounded towards zero. + * + * Counterpart to Solidity's `/` operator. Note: this function uses a + * `revert` opcode (which leaves remaining gas untouched) while Solidity + * uses an invalid opcode to revert (consuming all remaining gas). + * + * Requirements: + * - The divisor cannot be zero. + * NOTE: This is a feature of the next version of OpenZeppelin Contracts. + * @dev Get it via `npm install @openzeppelin/contracts@next`. + */ + function div(uint256 a, uint256 b, string memory errorMessage) internal pure returns (uint256) { + // Solidity only automatically asserts when dividing by 0 + require(b > 0, errorMessage); + uint256 c = a / b; + // assert(a == b * c + a % b); // There is no case in which this doesn't hold + + return c; + } + + /** + * @dev Returns the remainder of dividing two unsigned integers. (unsigned integer modulo), + * Reverts when dividing by zero. + * + * Counterpart to Solidity's `%` operator. This function uses a `revert` + * opcode (which leaves remaining gas untouched) while Solidity uses an + * invalid opcode to revert (consuming all remaining gas). + * + * Requirements: + * - The divisor cannot be zero. + */ + function mod(uint256 a, uint256 b) internal pure returns (uint256) { + return mod(a, b, "SafeMath: modulo by zero"); + } + + /** + * @dev Returns the remainder of dividing two unsigned integers. (unsigned integer modulo), + * Reverts with custom message when dividing by zero. + * + * Counterpart to Solidity's `%` operator. This function uses a `revert` + * opcode (which leaves remaining gas untouched) while Solidity uses an + * invalid opcode to revert (consuming all remaining gas). + * + * Requirements: + * - The divisor cannot be zero. + * + * NOTE: This is a feature of the next version of OpenZeppelin Contracts. + * @dev Get it via `npm install @openzeppelin/contracts@next`. + */ + function mod(uint256 a, uint256 b, string memory errorMessage) internal pure returns (uint256) { + require(b != 0, errorMessage); + return a % b; + } +} diff --git a/networks/mainnet-abi.json b/networks/mainnet-abi.json index dcaf6a272..81ea7cfb9 100644 --- a/networks/mainnet-abi.json +++ b/networks/mainnet-abi.json @@ -1937,6 +1937,21 @@ } ], "PriceOracleProxy": [ + { + "constant": true, + "inputs": [], + "name": "cEthAddress", + "outputs": [ + { + "name": "", + "type": "address" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function", + "signature": "0x2ed58e15" + }, { "constant": true, "inputs": [], @@ -1970,7 +1985,7 @@ { "constant": true, "inputs": [], - "name": "cEtherAddress", + "name": "makerUsdOracleKey", "outputs": [ { "name": "", @@ -1980,7 +1995,22 @@ "payable": false, "stateMutability": "view", "type": "function", - "signature": "0xde836acf" + "signature": "0xbc8a4ef4" + }, + { + "constant": true, + "inputs": [], + "name": "cDaiAddress", + "outputs": [ + { + "name": "", + "type": "address" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function", + "signature": "0xf2c65bf9" }, { "constant": true, @@ -2017,6 +2047,21 @@ "type": "function", "signature": "0xfe10c98d" }, + { + "constant": true, + "inputs": [], + "name": "cUsdcAddress", + "outputs": [ + { + "name": "", + "type": "address" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function", + "signature": "0xff11439b" + }, { "inputs": [ { @@ -2028,7 +2073,15 @@ "type": "address" }, { - "name": "cEtherAddress_", + "name": "cEthAddress_", + "type": "address" + }, + { + "name": "cUsdcAddress_", + "type": "address" + }, + { + "name": "cDaiAddress_", "type": "address" } ], diff --git a/networks/mainnet.json b/networks/mainnet.json index e38e96a61..4b420f3f2 100644 --- a/networks/mainnet.json +++ b/networks/mainnet.json @@ -3,7 +3,7 @@ "ZRX": "0xE41d2489571d322189246DaFA5ebDe1F4699F498", "cUSDC": "0x39AA39c021dfbaE8faC545936693aC917d5E7563", "PriceOracle": "0x02557a5e05defeffd4cae6d83ea3d173b272c904", - "PriceOracleProxy": "0x28F829F473638ba82710c8404A778f9a66029aAD", + "PriceOracleProxy": "0x2C9e6BDAA0EF0284eECef0e0Cc102dcDEaE4887e", "Maximillion": "0xf859A1AD94BcF445A406B892eF0d3082f4174088", "cDAI": "0xF5DCe57282A584D2746FaF1593d3121Fcac444dC", "DAI": "0x89d24a6b4ccb1b6faa2625fe562bdd9a23260359", @@ -27,7 +27,7 @@ "Blocks": { "cUSDC": 7710760, "PriceOracle": 6747538, - "PriceOracleProxy": 7710793, + "PriceOracleProxy": 8493386, "Maximillion": 7710775, "cDAI": 7710752, "StdComptroller": 7710672, @@ -41,6 +41,13 @@ "cZRX": 7710733, "cWBTC": 8163813 }, + "PriceOracleProxy": { + "description": "Price Oracle Proxy", + "cETH": "0x4Ddc2D193948926D02f9B1fE9e1daa0718270ED5", + "cUSDC": "0x39AA39c021dfbaE8faC545936693aC917d5E7563", + "cDAI": "0xF5DCe57282A584D2746FaF1593d3121Fcac444dC", + "address": "0x2C9e6BDAA0EF0284eECef0e0Cc102dcDEaE4887e" + }, "Maximillion": { "description": "Maximillion", "cEtherAddress": "0x4Ddc2D193948926D02f9B1fE9e1daa0718270ED5", @@ -59,7 +66,7 @@ }, "Constructors": { "cUSDC": "0x000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb480000000000000000000000003d9819210a31b4961b30ef54be2aed79b9c9cd3b000000000000000000000000c64c4cba055efa614ce01f4bad8a9f519c4f8fab0000000000000000000000000000000000000000000000000000b5e620f4800000000000000000000000000000000000000000000000000000000000000000e0000000000000000000000000000000000000000000000000000000000000012000000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000011436f6d706f756e642055534420436f696e00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000056355534443000000000000000000000000000000000000000000000000000000", - "PriceOracleProxy": "0x0000000000000000000000003d9819210a31b4961b30ef54be2aed79b9c9cd3b00000000000000000000000002557a5e05defeffd4cae6d83ea3d173b272c9040000000000000000000000004ddc2d193948926d02f9b1fe9e1daa0718270ed5", + "PriceOracleProxy": "0x0000000000000000000000003d9819210a31b4961b30ef54be2aed79b9c9cd3b00000000000000000000000002557a5e05defeffd4cae6d83ea3d173b272c9040000000000000000000000004ddc2d193948926d02f9b1fe9e1daa0718270ed500000000000000000000000039aa39c021dfbae8fac545936693ac917d5e7563000000000000000000000000f5dce57282a584d2746faf1593d3121fcac444dc", "Maximillion": "0x0000000000000000000000004ddc2d193948926d02f9b1fe9e1daa0718270ed5", "cDAI": "0x00000000000000000000000089d24a6b4ccb1b6faa2625fe562bdd9a232603590000000000000000000000003d9819210a31b4961b30ef54be2aed79b9c9cd3b000000000000000000000000a1046abfc2598f48c44fb320d281d3f3c0733c9a000000000000000000000000000000000000000000a56fa5b99019a5c800000000000000000000000000000000000000000000000000000000000000000000e000000000000000000000000000000000000000000000000000000000000001200000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000c436f6d706f756e6420446169000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000046344414900000000000000000000000000000000000000000000000000000000", "StdComptroller": "0x", diff --git a/package.json b/package.json index 3776f9507..8f161e436 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { - "name": "compound-money-market", + "name": "compound-protocol", "version": "0.2.1", "description": "The Compound Protocol", "main": "index.js", diff --git a/scenario/src/Builder/PriceOracleProxyBuilder.ts b/scenario/src/Builder/PriceOracleProxyBuilder.ts index 4b3033429..e88f63741 100644 --- a/scenario/src/Builder/PriceOracleProxyBuilder.ts +++ b/scenario/src/Builder/PriceOracleProxyBuilder.ts @@ -15,31 +15,34 @@ export interface PriceOracleProxyData { contract?: PriceOracleProxy, description: string, address?: string, - cEther: string, + cETH: string, cUSDC: string, + cDAI: string } export async function buildPriceOracleProxy(world: World, from: string, event: Event): Promise<{world: World, priceOracleProxy: PriceOracleProxy, invokation: Invokation}> { const fetchers = [ - new Fetcher<{comptroller: AddressV, priceOracle: AddressV, cEther: AddressV, cUSDC: AddressV}, PriceOracleProxyData>(` + new Fetcher<{comptroller: AddressV, priceOracle: AddressV, cETH: AddressV, cUSDC: AddressV, cDAI: AddressV}, PriceOracleProxyData>(` #### Price Oracle Proxy - * "Deploy " - The Price Oracle which proxies to a backing oracle - * E.g. "PriceOracleProxy Deploy (Unitroller Address) (PriceOracle Address) cETH cUSDC" + * "Deploy " - The Price Oracle which proxies to a backing oracle + * E.g. "PriceOracleProxy Deploy (Unitroller Address) (PriceOracle Address) cETH cUSDC cDAI" `, "PriceOracleProxy", [ new Arg("comptroller", getAddressV), new Arg("priceOracle", getAddressV), - new Arg("cEther", getAddressV), - new Arg("cUSDC", getAddressV) + new Arg("cETH", getAddressV), + new Arg("cUSDC", getAddressV), + new Arg("cDAI", getAddressV) ], - async (world, {comptroller, priceOracle, cEther, cUSDC}) => { + async (world, {comptroller, priceOracle, cETH, cUSDC, cDAI}) => { return { - invokation: await PriceOracleProxyContract.deploy(world, from, [comptroller.val, priceOracle.val, cEther.val, cUSDC.val]), + invokation: await PriceOracleProxyContract.deploy(world, from, [comptroller.val, priceOracle.val, cETH.val, cUSDC.val, cDAI.val]), description: "Price Oracle Proxy", - cEther: cEther.val, - cUSDC: cUSDC.val + cETH: cETH.val, + cUSDC: cUSDC.val, + cDAI: cDAI.val }; }, {catchall: true} diff --git a/scenario/src/Contract/PriceOracle.ts b/scenario/src/Contract/PriceOracle.ts index 4ea9f8d58..04d283668 100644 --- a/scenario/src/Contract/PriceOracle.ts +++ b/scenario/src/Contract/PriceOracle.ts @@ -5,6 +5,7 @@ import {encodedNumber} from '../Encoding'; interface PriceOracleMethods { assetPrices(asset: string): Callable setUnderlyingPrice(cToken: string, amount: encodedNumber): Sendable + setDirectPrice(address: string, amount: encodedNumber): Sendable // Anchor Price Oracle getPrice(asset: string): Callable diff --git a/scenario/src/Event/PriceOracleEvent.ts b/scenario/src/Event/PriceOracleEvent.ts index 252d83517..fadbd9dca 100644 --- a/scenario/src/Event/PriceOracleEvent.ts +++ b/scenario/src/Event/PriceOracleEvent.ts @@ -47,6 +47,14 @@ async function setPrice(world: World, from: string, priceOracle: PriceOracle, cT ); } +async function setDirectPrice(world: World, from: string, priceOracle: PriceOracle, address: string, amount: NumberV): Promise { + return addAction( + world, + `Set price oracle price for ${address} to ${amount.show()}`, + await invoke(world, priceOracle.methods.setDirectPrice(address, amount.encode()), from) + ); +} + async function verifyPriceOracle(world: World, priceOracle: PriceOracle, apiKey: string, contractName: string): Promise { if (world.isLocalNetwork()) { world.printer.printLine(`Politely declining to verify on local network: ${world.network}.`); @@ -84,7 +92,9 @@ export function priceOracleCommands() { new Arg("params", getEventV, {variadic: true}) ], (world, from, {params}) => setPriceOracleFn(world, params.val) - ),new Command<{priceOracle: PriceOracle, cToken: AddressV, amount: NumberV}>(` + ), + + new Command<{priceOracle: PriceOracle, cToken: AddressV, amount: NumberV}>(` #### SetPrice * "SetPrice " - Sets the per-ether price for the given cToken @@ -98,6 +108,22 @@ export function priceOracleCommands() { ], (world, from, {priceOracle, cToken, amount}) => setPrice(world, from, priceOracle, cToken.val, amount) ), + + new Command<{priceOracle: PriceOracle, address: AddressV, amount: NumberV}>(` + #### SetDirectPrice + + * "SetDirectPrice
" - Sets the per-ether price for the given cToken + * E.g. "PriceOracle SetDirectPrice (Address Zero) 1.0" + `, + "SetDirectPrice", + [ + new Arg("priceOracle", getPriceOracle, {implicit: true}), + new Arg("address", getAddressV), + new Arg("amount", getExpNumberV) + ], + (world, from, {priceOracle, address, amount}) => setDirectPrice(world, from, priceOracle, address.val, amount) + ), + new View<{priceOracle: PriceOracle, apiKey: StringV, contractName: StringV}>(` #### Verify diff --git a/scenario/src/Event/PriceOracleProxyEvent.ts b/scenario/src/Event/PriceOracleProxyEvent.ts index 57a8e3e9d..f5654cd78 100644 --- a/scenario/src/Event/PriceOracleProxyEvent.ts +++ b/scenario/src/Event/PriceOracleProxyEvent.ts @@ -59,6 +59,7 @@ export function priceOracleProxyCommands() { ], (world, from, {params}) => genPriceOracleProxy(world, from, params.val) ), + new View<{priceOracleProxy: PriceOracleProxy, apiKey: StringV, contractName: StringV}>(` #### Verify diff --git a/spec/scenario/CoreMacros b/spec/scenario/CoreMacros index c55ce3956..784972468 100644 --- a/spec/scenario/CoreMacros +++ b/spec/scenario/CoreMacros @@ -11,7 +11,7 @@ Macro PricedComptroller closeFactor=0.1 maxAssets=20 PriceOracle Deploy Simple ComptrollerImpl Deploy Scenario ScenComptroller Unitroller SetPendingImpl ScenComptroller - PriceOracleProxy Deploy (Unitroller Address) (PriceOracle Address) (Address Zero) (Address Zero) -- Final argument is a Junk address, if listing cEther use ListedEtherToken to replace proxy + PriceOracleProxy Deploy (Unitroller Address) (PriceOracle Address) (Address Zero) (Address Zero) (Address Zero) -- Final argument is a Junk address, if listing cEther use ListedEtherToken to replace proxy ComptrollerImpl ScenComptroller Become (PriceOracleProxy Address) closeFactor maxAssets Macro NewComptroller price=1.0 closeFactor=0.1 maxAssets=20 @@ -19,7 +19,7 @@ Macro NewComptroller price=1.0 closeFactor=0.1 maxAssets=20 PriceOracle Deploy Fixed price ComptrollerImpl Deploy Scenario ScenComptroller Unitroller SetPendingImpl ScenComptroller - PriceOracleProxy Deploy (Unitroller Address) (PriceOracle Address) (Address Zero) (Address Zero) -- Junk address, if listing cEther use ListedEtherToken to replace proxy + PriceOracleProxy Deploy (Unitroller Address) (PriceOracle Address) (Address Zero) (Address Zero) (Address Zero) -- Junk address, if listing cEther use ListedEtherToken to replace proxy ComptrollerImpl ScenComptroller Become (PriceOracleProxy Address) closeFactor maxAssets Macro NewCToken erc20 cToken borrowRate=0.0005 initialExchangeRate=2e9 decimals=8 tokenType=Standard cTokenType=Scenario @@ -38,7 +38,7 @@ Macro ListedCToken erc20 cToken borrowRate=0.0005 initialExchangeRate=2e9 decima Macro ListedEtherToken cToken borrowRate=0.0005 initialExchangeRate=2e9 decimals=8 NewEtherToken cToken borrowRate initialExchangeRate decimals Comptroller SupportMarket cToken - PriceOracleProxy Deploy (Unitroller Address) (PriceOracle Address) (Address cETH) (Address Zero) + PriceOracleProxy Deploy (Unitroller Address) (PriceOracle Address) (Address cETH) (Address Zero) (Address Zero) Comptroller SetPriceOracle (PriceOracleProxy Address) Macro ListedEtherTokenMinted cToken borrowRate=0.0005 initialExchangeRate=2e9 decimals=8 diff --git a/spec/scenario/PriceOracleProxy.scen b/spec/scenario/PriceOracleProxy.scen new file mode 100644 index 000000000..0f956be13 --- /dev/null +++ b/spec/scenario/PriceOracleProxy.scen @@ -0,0 +1,39 @@ +Macro SetupPriceOracleProxy + Unitroller Deploy + PriceOracle Deploy Simple + ComptrollerImpl Deploy Scenario ScenComptroller + Unitroller SetPendingImpl ScenComptroller + PriceOracleProxy Deploy (Unitroller Address) (PriceOracle Address) (Address Zero) (Address Zero) (Address Zero) + ComptrollerImpl ScenComptroller Become (PriceOracleProxy Address) 0.1 20 + NewEtherToken cETH + NewCToken USDC cUSDC + NewCToken DAI cDAI + Comptroller SupportMarket cETH + Comptroller SupportMarket cUSDC + Comptroller SupportMarket cDAI + PriceOracleProxy Deploy (Unitroller Address) (PriceOracle Address) (Address cETH) (Address cUSDC) (Address cDAI) + Comptroller SetPriceOracle (PriceOracleProxy Address) + +Test "computes address(2) / address(1) * maker usd price for cdai when within ratio bound" + SetupPriceOracleProxy + PriceOracle SetPrice cDAI 0.005 -- the maker usd/eth price is at the dai address + PriceOracle SetDirectPrice (Address 0x0000000000000000000000000000000000000001) 5740564708572881000000000000 + PriceOracle SetDirectPrice (Address 0x0000000000000000000000000000000000000002) 5842307360923634 + Assert Equal (PriceOracleProxy Price cDAI) 0.005088617285507479e18 + Assert Equal (PriceOracleProxy Price cUSDC) 0.005e30 + +Test "computes address(2) / address(1) * maker usd price for cdai when below ratio bound" + SetupPriceOracleProxy + PriceOracle SetPrice cDAI 0.005 -- the maker usd/eth price is at the dai address + PriceOracle SetDirectPrice (Address 0x0000000000000000000000000000000000000001) 57405647085728810000000000000 + PriceOracle SetDirectPrice (Address 0x0000000000000000000000000000000000000002) 5842307360923634 + Assert Equal (PriceOracleProxy Price cDAI) 0.00475e18 + Assert Equal (PriceOracleProxy Price cUSDC) 0.005e30 + +Test "computes address(2) / address(1) * maker usd price for cdai when above ratio bound" + SetupPriceOracleProxy + PriceOracle SetPrice cDAI 0.005 -- the maker usd/eth price is at the dai address - will be scaled by 1e18 + PriceOracle SetDirectPrice (Address 0x0000000000000000000000000000000000000001) 5740564708572881000000000000 + PriceOracle SetDirectPrice (Address 0x0000000000000000000000000000000000000002) 58423073609236340 + Assert Equal (PriceOracleProxy Price cDAI) 0.00525e18 + Assert Equal (PriceOracleProxy Price cUSDC) 0.005e30 diff --git a/test/PriceOracleProxyTest.js b/test/PriceOracleProxyTest.js index 80efc806d..9b8227ca9 100644 --- a/test/PriceOracleProxyTest.js +++ b/test/PriceOracleProxyTest.js @@ -9,7 +9,6 @@ const { } = require('./Utils/MochaTruffle'); const { - makeComptroller, makeCToken, makePriceOracle, } = require('./Utils/Compound'); @@ -17,24 +16,23 @@ const { const OraclePriceOracleProxy = getContract('PriceOracleProxy'); contract('PriceOracleProxy', function([root, ...accounts]) { - let oracle, comptroller, backingOracle, cEther, cErc20, cUsdc, fakeUSDC; + let oracle, backingOracle, cEth, cUsdc, cDai, cOther; before(async () =>{ - cEther = await makeCToken({kind: "cether", - comptrollerOpts: { kind: "v1-no-proxy"}, - supportMarket: true}); - - cUsdc = await makeCToken({comptroller: cEther.comptroller, - supportMarket: true}); + cEth = await makeCToken({kind: "cether", comptrollerOpts: {kind: "v1-no-proxy"}, supportMarket: true}); + cUsdc = await makeCToken({comptroller: cEth.comptroller, supportMarket: true}); + cDai = await makeCToken({comptroller: cEth.comptroller, supportMarket: true}); + cOther = await makeCToken({comptroller: cEth.comptroller, supportMarket: true}); backingOracle = await makePriceOracle(); oracle = await OraclePriceOracleProxy .deploy({ arguments: [ - cEther.comptroller._address, + cEth.comptroller._address, backingOracle._address, - cEther._address, - cUsdc._address + cEth._address, + cUsdc._address, + cDai._address ]}) .send({from: root}); }); @@ -42,7 +40,7 @@ contract('PriceOracleProxy', function([root, ...accounts]) { describe("constructor", async () => { it("sets address of comptroller", async () => { let configuredComptroller = await call(oracle, "comptroller"); - assert.equal(configuredComptroller, cEther.comptroller._address); + assert.equal(configuredComptroller, cEth.comptroller._address); }); it("sets address of v1 oracle", async () => { @@ -50,16 +48,21 @@ contract('PriceOracleProxy', function([root, ...accounts]) { assert.equal(configuredOracle, backingOracle._address); }); - it("sets address of cEther", async () => { - let configuredCEther = await call(oracle, "cEtherAddress"); - assert.equal(configuredCEther, cEther._address); + it("sets address of cEth", async () => { + let configuredCEther = await call(oracle, "cEthAddress"); + assert.equal(configuredCEther, cEth._address); }); it("sets address of cUSDC", async () => { let configuredCUSD = await call(oracle, "cUsdcAddress"); assert.equal(configuredCUSD, cUsdc._address); }); - }); + + it("sets address of cDAI", async () => { + let configuredCDAI = await call(oracle, "cDaiAddress"); + assert.equal(configuredCDAI, cDai._address); + }); +}); describe("getUnderlyingPrice", async () => { let setAndVerifyBackingPrice = async (cToken, price) => { @@ -81,49 +84,52 @@ contract('PriceOracleProxy', function([root, ...accounts]) { assert.equal(Number(proxyPrice), price * 1e18);; }; - it("always returns 1e18 for cEther", async () => { - await readAndVerifyProxyPrice(cEther, 1); + it("always returns 1e18 for cEth", async () => { + await readAndVerifyProxyPrice(cEth, 1); }); - it("checks address(1) for cusdc", async () => { - await send( - backingOracle, - "setDirectPrice", - [address(1), etherMantissa(50)]); - - await readAndVerifyProxyPrice(cUsdc, 50); + it("proxies to v1 oracle for cusdc", async () => { + await setAndVerifyBackingPrice(cDai, 50); + await readAndVerifyProxyPrice(cUsdc, 50e12); }); - it("proxies to v1 oracle for listed cErc20's", async () => { - let listedToken = await makeCToken({comptroller: cEther.comptroller, supportMarket: true}); + it("computes address(2) / address(1) * maker usd price for cdai", async () => { + await setAndVerifyBackingPrice(cDai, 5); + + // 0.95 < ratio < 1.05 + await send(backingOracle, "setDirectPrice", [address(1), etherMantissa(1e12)]); + await send(backingOracle, "setDirectPrice", [address(2), etherMantissa(1.03)]); + await readAndVerifyProxyPrice(cDai, 1.03 * 5); - await setAndVerifyBackingPrice(listedToken, 12); - await readAndVerifyProxyPrice(listedToken, 12); + // ratio <= 0.95 + await send(backingOracle, "setDirectPrice", [address(1), etherMantissa(5e12)]); + await readAndVerifyProxyPrice(cDai, 0.95 * 5); - await setAndVerifyBackingPrice(listedToken, 37); - await readAndVerifyProxyPrice(listedToken, 37); + // 1.05 <= ratio + await send(backingOracle, "setDirectPrice", [address(1), etherMantissa(5e11)]); + await readAndVerifyProxyPrice(cDai, 1.05 * 5); }); - describe("returns 0 for unlisted tokens", async () => { - it("right comptroller, not supported", async () => { - let unlistedToken = await makeCToken({comptroller: comptroller, - supportMarket: false}); + it("proxies for whitelisted tokens", async () => { + await setAndVerifyBackingPrice(cOther, 11); + await readAndVerifyProxyPrice(cOther, 11); - await setAndVerifyBackingPrice(unlistedToken, 12); - await readAndVerifyProxyPrice(unlistedToken, 0); - }); + await setAndVerifyBackingPrice(cOther, 37); + await readAndVerifyProxyPrice(cOther, 37); + }); - it("wrong comptroller", async () => { - let wrongNetworkToken = await makeCToken({supportMarket: true}); - await setAndVerifyBackingPrice(wrongNetworkToken, 10); + it("returns 0 for non-whitelisted token", async () => { + let unlistedToken = await makeCToken({comptroller: cEth.comptroller}); - await readAndVerifyProxyPrice(wrongNetworkToken, 0); - }); + await setAndVerifyBackingPrice(unlistedToken, 12); + await readAndVerifyProxyPrice(unlistedToken, 0); + }); - it("not even a cToken", async () => { - let proxyPrice = await call(oracle, "getUnderlyingPrice", [root]); - assert.equal(Number(proxyPrice), 0); - }); + it("returns 0 for wrong comptroller", async () => { + let wrongNetworkToken = await makeCToken({supportMarket: true}); + await setAndVerifyBackingPrice(wrongNetworkToken, 10); + await readAndVerifyProxyPrice(wrongNetworkToken, 0); }); + }); }); diff --git a/test/Utils/Compound.js b/test/Utils/Compound.js index e15ea5cae..27049f0d3 100644 --- a/test/Utils/Compound.js +++ b/test/Utils/Compound.js @@ -172,15 +172,12 @@ async function makePriceOracle(opts = {}) { const cEther = opts.cEther || await makeCToken({ kind: 'cether', supportMarket: true, - comptrollerOpts: { - priceOracleOpts: {kind: 'simple'} - }, ...opts.cEtherOpts}); + ...opts.cEtherOpts}); const comptroller = cEther.comptroller; const priceOracle = comptroller.priceOracle; const PriceOracleProxy = getContract('PriceOracleProxy'); const priceOracleProxy = await PriceOracleProxy.deploy({ arguments: [ - comptroller._address, priceOracle._address, cEther._address ]}).send({from: root});