forked from compound-finance/comet
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Multiplicative Price Feed (compound-finance#757)
This generalizes the logic used in the recently audited [`WBTCPriceFeed`](compound-finance#737) to create a `MultiplicativePriceFeed` that derives its price from multiplying the prices from two other price feeds together. This is a flexible wrapper price feed that can be used to generate prices for any asset that does not have a price feed that fits Comet's expected price denomination. e.g. if we wanted to add `cbETH` to `cUSDCv3`, there is no Chainlink price feed for `cbETH / USD`, so we would need to multiply the `cbETH / ETH` and `ETH / USD` price feeds together to get `cbETH / USD`
- Loading branch information
1 parent
9a962f0
commit ccb6e9b
Showing
6 changed files
with
276 additions
and
8 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,89 @@ | ||
// SPDX-License-Identifier: BUSL-1.1 | ||
pragma solidity 0.8.15; | ||
|
||
import "../vendor/@chainlink/contracts/src/v0.8/interfaces/AggregatorV3Interface.sol"; | ||
import "../IPriceFeed.sol"; | ||
|
||
/** | ||
* @title Multiplicative price feed | ||
* @notice A custom price feed that multiplies the prices from two price feeds and returns the result | ||
* @author Compound | ||
*/ | ||
contract MultiplicativePriceFeed is IPriceFeed { | ||
/** Custom errors **/ | ||
error BadDecimals(); | ||
error InvalidInt256(); | ||
|
||
/// @notice Version of the price feed | ||
uint public constant VERSION = 1; | ||
|
||
/// @notice Description of the price feed | ||
string public override description; | ||
|
||
/// @notice Number of decimals for returned prices | ||
uint8 public immutable override decimals; | ||
|
||
/// @notice Chainlink price feed A | ||
address public immutable priceFeedA; | ||
|
||
/// @notice Chainlink price feed B | ||
address public immutable priceFeedB; | ||
|
||
/// @notice Combined scale of the two underlying Chainlink price feeds | ||
int public immutable combinedScale; | ||
|
||
/// @notice Scale of this price feed | ||
int public immutable priceFeedScale; | ||
|
||
/** | ||
* @notice Construct a new multiplicative price feed | ||
* @param priceFeedA_ The address of the first price feed to fetch prices from | ||
* @param priceFeedB_ The address of the second price feed to fetch prices from | ||
* @param decimals_ The number of decimals for the returned prices | ||
* @param description_ The description of the price feed | ||
**/ | ||
constructor(address priceFeedA_, address priceFeedB_, uint8 decimals_, string memory description_) { | ||
priceFeedA = priceFeedA_; | ||
priceFeedB = priceFeedB_; | ||
uint8 priceFeedADecimals = AggregatorV3Interface(priceFeedA_).decimals(); | ||
uint8 priceFeedBDecimals = AggregatorV3Interface(priceFeedB_).decimals(); | ||
combinedScale = signed256(10 ** (priceFeedADecimals + priceFeedBDecimals)); | ||
|
||
if (decimals_ > 18) revert BadDecimals(); | ||
decimals = decimals_; | ||
description = description_; | ||
priceFeedScale = int256(10 ** decimals); | ||
} | ||
|
||
/** | ||
* @notice Calculates the latest round data using data from the two price feeds | ||
* @return roundId Round id from price feed B | ||
* @return answer Latest price | ||
* @return startedAt Timestamp when the round was started; passed on from price feed B | ||
* @return updatedAt Timestamp when the round was last updated; passed on from price feed B | ||
* @return answeredInRound Round id in which the answer was computed; passed on from price feed B | ||
* @dev Note: Only the `answer` really matters for downstream contracts that use this price feed (e.g. Comet) | ||
**/ | ||
function latestRoundData() override external view returns (uint80, int256, uint256, uint256, uint80) { | ||
(, int256 priceA, , , ) = AggregatorV3Interface(priceFeedA).latestRoundData(); | ||
(uint80 roundId_, int256 priceB, uint256 startedAt_, uint256 updatedAt_, uint80 answeredInRound_) = AggregatorV3Interface(priceFeedB).latestRoundData(); | ||
|
||
if (priceA <= 0 || priceB <= 0) return (roundId_, 0, startedAt_, updatedAt_, answeredInRound_); | ||
|
||
int256 price = priceA * priceB * priceFeedScale / combinedScale; | ||
return (roundId_, price, startedAt_, updatedAt_, answeredInRound_); | ||
} | ||
|
||
function signed256(uint256 n) internal pure returns (int256) { | ||
if (n > uint256(type(int256).max)) revert InvalidInt256(); | ||
return int256(n); | ||
} | ||
|
||
/** | ||
* @notice Price for the latest round | ||
* @return The version of the price feed contract | ||
**/ | ||
function version() external pure returns (uint256) { | ||
return VERSION; | ||
} | ||
} |
4 changes: 2 additions & 2 deletions
4
test/constant-price-feed-test.ts → test/pricefeeds/constant-price-feed-test.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,179 @@ | ||
import { ethers, exp, expect } from '../helpers'; | ||
import { | ||
SimplePriceFeed__factory, | ||
MultiplicativePriceFeed__factory | ||
} from '../../build/types'; | ||
|
||
export async function makeMultiplicativePriceFeed({ priceA, priceB, decimalsA = 8, decimalsB = 8 }) { | ||
const SimplePriceFeedFactory = (await ethers.getContractFactory( | ||
'SimplePriceFeed' | ||
)) as SimplePriceFeed__factory; | ||
const PriceFeedA = await SimplePriceFeedFactory.deploy(priceA, decimalsA); | ||
await PriceFeedA.deployed(); | ||
|
||
const PriceFeedB = await SimplePriceFeedFactory.deploy(priceB, decimalsB); | ||
await PriceFeedB.deployed(); | ||
|
||
const MultiplicativePriceFeedFactory = (await ethers.getContractFactory( | ||
'MultiplicativePriceFeed' | ||
)) as MultiplicativePriceFeed__factory; | ||
const MultiplicativePriceFeed = await MultiplicativePriceFeedFactory.deploy( | ||
PriceFeedA.address, | ||
PriceFeedB.address, | ||
8, | ||
'Multiplicative Price Feed' | ||
); | ||
await MultiplicativePriceFeed.deployed(); | ||
|
||
return { | ||
PriceFeedA, | ||
PriceFeedB, | ||
MultiplicativePriceFeed | ||
}; | ||
} | ||
|
||
const testCases = [ | ||
// Existing test cases from WBTC price feed | ||
{ | ||
priceA: exp(1, 8), | ||
priceB: exp(30_000, 8), | ||
result: exp(30_000, 8) | ||
}, | ||
{ | ||
priceA: exp(2.123456, 8), | ||
priceB: exp(31_333.123, 8), | ||
result: 6653450803308n | ||
}, | ||
{ | ||
priceA: exp(100, 8), | ||
priceB: exp(30_000, 8), | ||
result: exp(3_000_000, 8) | ||
}, | ||
{ | ||
priceA: exp(0.9999, 8), | ||
priceB: exp(30_000, 8), | ||
result: exp(29_997, 8) | ||
}, | ||
{ | ||
priceA: exp(0.987937, 8), | ||
priceB: exp(31_947.71623, 8), | ||
result: 3156233092911n | ||
}, | ||
{ | ||
priceA: exp(0.5, 8), | ||
priceB: exp(30_000, 8), | ||
result: exp(15_000, 8) | ||
}, | ||
{ | ||
priceA: exp(0.00555, 8), | ||
priceB: exp(30_000, 8), | ||
result: exp(166.5, 8) | ||
}, | ||
{ | ||
priceA: exp(0, 8), | ||
priceB: exp(30_000, 8), | ||
result: exp(0, 8) | ||
}, | ||
{ | ||
priceA: exp(1, 8), | ||
priceB: exp(0, 8), | ||
result: exp(0, 8) | ||
}, | ||
{ | ||
priceA: exp(0, 8), | ||
priceB: exp(0, 8), | ||
result: exp(0, 8) | ||
}, | ||
// e.g. cbETH / ETH (18 decimals) and ETH / USD (8 decimals) | ||
{ | ||
priceA: exp(1, 18), | ||
priceB: exp(1800, 8), | ||
decimalsA: 18, | ||
decimalsB: 8, | ||
result: exp(1800, 8) | ||
}, | ||
{ | ||
priceA: exp(1.25, 18), | ||
priceB: exp(1800, 8), | ||
decimalsA: 18, | ||
decimalsB: 8, | ||
result: exp(2250, 8) | ||
}, | ||
{ | ||
priceA: exp(0.72, 18), | ||
priceB: exp(1800, 8), | ||
decimalsA: 18, | ||
decimalsB: 8, | ||
result: exp(1296, 8) | ||
}, | ||
]; | ||
|
||
describe('Multiplicative price feed', function() { | ||
it('reverts if constructed with bad decimals', async () => { | ||
const SimplePriceFeedFactory = (await ethers.getContractFactory( | ||
'SimplePriceFeed' | ||
)) as SimplePriceFeed__factory; | ||
const PriceFeedA = await SimplePriceFeedFactory.deploy(exp(1, 8), 8); | ||
await PriceFeedA.deployed(); | ||
|
||
const PriceFeedB = await SimplePriceFeedFactory.deploy(exp(30_000), 8); | ||
await PriceFeedB.deployed(); | ||
|
||
const MultiplicativePriceFeed = (await ethers.getContractFactory( | ||
'MultiplicativePriceFeed' | ||
)) as MultiplicativePriceFeed__factory; | ||
await expect( | ||
MultiplicativePriceFeed.deploy( | ||
PriceFeedA.address, | ||
PriceFeedB.address, | ||
20, // decimals_ is too high | ||
'Multiplicative Price Feed' | ||
) | ||
).to.be.revertedWith("custom error 'BadDecimals()'"); | ||
}); | ||
|
||
describe('latestRoundData', function() { | ||
for (const { priceA, priceB, decimalsA, decimalsB, result } of testCases) { | ||
it(`priceA (${priceA}) with ${decimalsA ?? 8} decimals, priceB (${priceB}) with ${decimalsB ?? 8} decimals -> ${result}`, async () => { | ||
const { MultiplicativePriceFeed } = await makeMultiplicativePriceFeed({ priceA, priceB, decimalsA, decimalsB }); | ||
const latestRoundData = await MultiplicativePriceFeed.latestRoundData(); | ||
const price = latestRoundData[1].toBigInt(); | ||
|
||
expect(price).to.eq(result); | ||
}); | ||
} | ||
|
||
it('passes along roundId, startedAt, updatedAt and answeredInRound values from price feed B', async () => { | ||
const { PriceFeedB, MultiplicativePriceFeed } = await makeMultiplicativePriceFeed({ | ||
priceA: exp(1, 18), | ||
priceB: exp(30_000, 18) | ||
}); | ||
|
||
await PriceFeedB.setRoundData( | ||
exp(15, 18), // roundId_, | ||
1, // answer_, | ||
exp(16, 8), // startedAt_, | ||
exp(17, 8), // updatedAt_, | ||
exp(18, 18) // answeredInRound_ | ||
); | ||
|
||
const roundData = await MultiplicativePriceFeed.latestRoundData(); | ||
|
||
expect(roundData[0].toBigInt()).to.eq(exp(15, 18)); | ||
expect(roundData[2].toBigInt()).to.eq(exp(16, 8)); | ||
expect(roundData[3].toBigInt()).to.eq(exp(17, 8)); | ||
expect(roundData[4].toBigInt()).to.eq(exp(18, 18)); | ||
}); | ||
}); | ||
|
||
it('getters return correct values', async () => { | ||
const { MultiplicativePriceFeed } = await makeMultiplicativePriceFeed({ | ||
priceA: exp(1, 18), | ||
priceB: exp(30_000, 18) | ||
}); | ||
|
||
expect(await MultiplicativePriceFeed.version()).to.eq(1); | ||
expect(await MultiplicativePriceFeed.description()).to.eq('Multiplicative Price Feed'); | ||
expect(await MultiplicativePriceFeed.decimals()).to.eq(8); | ||
}); | ||
}); |
4 changes: 2 additions & 2 deletions
4
test/scaling-price-feed-test.ts → test/pricefeeds/scaling-price-feed-test.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
4 changes: 2 additions & 2 deletions
4
test/wsteth-price-feed.ts → test/pricefeeds/wsteth-price-feed.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters