Skip to content

Commit

Permalink
Multiplicative Price Feed (compound-finance#757)
Browse files Browse the repository at this point in the history
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
kevincheng96 authored Aug 3, 2023
1 parent 9a962f0 commit ccb6e9b
Show file tree
Hide file tree
Showing 6 changed files with 276 additions and 8 deletions.
89 changes: 89 additions & 0 deletions contracts/pricefeeds/MultiplicativePriceFeed.sol
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;
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { ethers, exp, expect, getBlock } from './helpers';
import { ethers, exp, expect, getBlock } from '../helpers';
import {
ConstantPriceFeed__factory
} from '../build/types';
} from '../../build/types';

export async function makeConstantPriceFeed({ decimals, constantPrice }) {
const constantPriceFeedFactory = (await ethers.getContractFactory('ConstantPriceFeed')) as ConstantPriceFeed__factory;
Expand Down
179 changes: 179 additions & 0 deletions test/pricefeeds/multiplicative-price-feed.ts
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);
});
});
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { ethers, exp, expect } from './helpers';
import { ethers, exp, expect } from '../helpers';
import {
SimplePriceFeed__factory,
ScalingPriceFeed__factory
} from '../build/types';
} from '../../build/types';

export async function makeScalingPriceFeed({ price, priceFeedDecimals }) {
const SimplePriceFeedFactory = (await ethers.getContractFactory('SimplePriceFeed')) as SimplePriceFeed__factory;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { ethers, exp, expect } from './helpers';
import { ethers, exp, expect } from '../helpers';
import {
SimplePriceFeed__factory,
WBTCPriceFeed__factory
} from '../build/types';
} from '../../build/types';

export async function makeWBTCPriceFeed({ WBTCToBTCPrice, BTCToUSDPrice }) {
const SimplePriceFeedFactory = (await ethers.getContractFactory(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { ethers, exp, expect } from './helpers';
import { ethers, exp, expect } from '../helpers';
import {
SimplePriceFeed__factory,
SimpleWstETH__factory,
WstETHPriceFeed__factory
} from '../build/types';
} from '../../build/types';

export async function makeWstETH({ stEthPrice, tokensPerStEth }) {
const SimplePriceFeedFactory = (await ethers.getContractFactory('SimplePriceFeed')) as SimplePriceFeed__factory;
Expand Down

0 comments on commit ccb6e9b

Please sign in to comment.