Skip to content

Commit

Permalink
✨ price-feeds: add pool-based feed
Browse files Browse the repository at this point in the history
Co-authored-by: danilo neves cruz <cruzdanilo@gmail.com>
  • Loading branch information
santichez and cruzdanilo committed Jul 23, 2023
1 parent 19a99b7 commit 34d960c
Show file tree
Hide file tree
Showing 4 changed files with 158 additions and 0 deletions.
5 changes: 5 additions & 0 deletions .changeset/blue-pumpkins-fix.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@exactly/protocol": patch
---

✨ price-feeds: add pool-based feed
6 changes: 6 additions & 0 deletions .gas-snapshot
Original file line number Diff line number Diff line change
Expand Up @@ -330,6 +330,12 @@ PriceFeedDoubleTest:testPriceFeedDoubleReturningPrice() (gas: 26143)
PriceFeedDoubleTest:testPriceFeedDoubleWithActualOnChainValues() (gas: 30976)
PriceFeedDoubleTest:testPriceFeedDoubleWithNegativePriceShouldRevert() (gas: 46089)
PriceFeedDoubleTest:testPriceFeedDoubleWithUsdPriceFeed() (gas: 327246)
PriceFeedPoolTest:testPriceFeedPoolReturningPrice() (gas: 24098)
PriceFeedPoolTest:testPriceFeedPoolReturningPriceWithToken0False() (gas: 1787019)
PriceFeedPoolTest:testPriceFeedPoolWithAllDifferentDecimals() (gas: 1780149)
PriceFeedPoolTest:testPriceFeedPoolWithAllDifferentDecimalsWithToken0False() (gas: 1785255)
PriceFeedPoolTest:testPriceFeedPoolWithDifferentDecimals() (gas: 1780149)
PriceFeedPoolTest:testPriceFeedPoolWithDifferentDecimalsWithToken0False() (gas: 1785276)
PriceFeedWrapperTest:testPriceFeedWrapperReturningPrice() (gas: 16449)
PriceFeedWrapperTest:testPriceFeedWrapperReturningPriceAfterRebase() (gas: 22863)
PriceFeedWrapperTest:testPriceFeedWrapperWithActualOnChainValues() (gas: 30295)
Expand Down
53 changes: 53 additions & 0 deletions contracts/PriceFeedPool.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
// SPDX-License-Identifier: BUSL-1.1
pragma solidity 0.8.17;

import { ERC20 } from "solmate/src/tokens/ERC20.sol";
import { FixedPointMathLib } from "solmate/src/utils/FixedPointMathLib.sol";
import { IPriceFeed } from "./utils/IPriceFeed.sol";

contract PriceFeedPool is IPriceFeed {
using FixedPointMathLib for uint256;

/// @notice Base price feed where the price is fetched from.
IPriceFeed public immutable basePriceFeed;
/// @notice Base unit of pool's token0.
uint256 public immutable baseUnit0;
/// @notice Base unit of pool's token1.
uint256 public immutable baseUnit1;
/// @notice Number of decimals that the answer of this price feed has.
uint8 public immutable decimals;
/// @notice Whether the pool's token0 is the base price feed's asset.
bool public immutable token0;
/// @notice Pool where the exchange rate is fetched from.
IPool public immutable pool;

constructor(IPool pool_, IPriceFeed basePriceFeed_, bool token0_) {
pool = pool_;
token0 = token0_;
basePriceFeed = basePriceFeed_;
decimals = basePriceFeed_.decimals();
baseUnit0 = 10 ** pool_.token0().decimals();
baseUnit1 = 10 ** pool_.token1().decimals();
}

/// @notice Returns the price feed's latest value considering the pool's reserves (exchange rate).
/// @dev Value should only be used for display purposes since pool reserves can be easily manipulated.
function latestAnswer() external view returns (int256) {
int256 mainPrice = basePriceFeed.latestAnswer();
(uint256 reserve0, uint256 reserve1, ) = pool.getReserves();
return
int256(
token0
? uint256(mainPrice).mulDivDown((reserve0 * baseUnit1) / reserve1, baseUnit0)
: uint256(mainPrice).mulDivDown((reserve1 * baseUnit0) / reserve0, baseUnit1)
);
}
}

interface IPool {
function token0() external view returns (ERC20);

function token1() external view returns (ERC20);

function getReserves() external view returns (uint256 reserve0, uint256 reserve1, uint256 blockTimestampLast);
}
94 changes: 94 additions & 0 deletions test/solidity/PriceFeedPool.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
// SPDX-License-Identifier: BUSL-1.1
pragma solidity 0.8.17;

import { MockERC20 } from "solmate/src/test/utils/mocks/MockERC20.sol";
import { Test, stdError } from "forge-std/Test.sol";
import { FixedPointMathLib } from "solmate/src/utils/FixedPointMathLib.sol";
import { PriceFeedPool, IPriceFeed, IPool, ERC20 } from "../../contracts/PriceFeedPool.sol";
import { MockPriceFeed } from "../../contracts/mocks/MockPriceFeed.sol";

contract PriceFeedPoolTest is Test {
using FixedPointMathLib for uint256;

PriceFeedPool internal priceFeedPool;
MockPriceFeed internal ethPriceFeed;
MockPool internal mockPool;

function setUp() external {
mockPool = new MockPool(new MockERC20("WETH", "WETH", 18), new MockERC20("EXA", "EXA", 18), 100e18, 500e18);
ethPriceFeed = new MockPriceFeed(18, 2_000e18);
priceFeedPool = new PriceFeedPool(mockPool, ethPriceFeed, true);
}

function testPriceFeedPoolReturningPrice() external {
(uint256 reserve0, uint256 reserve1, ) = mockPool.getReserves();
uint256 usdPrice = uint256(ethPriceFeed.latestAnswer()).mulDivDown(reserve0, reserve1);
assertEq(usdPrice, 400e18);
assertEq(uint256(priceFeedPool.latestAnswer()), usdPrice);
}

function testPriceFeedPoolWithDifferentDecimals() external {
priceFeedPool = new PriceFeedPool(
new MockPool(new MockERC20("WETH", "WETH", 18), new MockERC20("EXA", "EXA", 8), 100e18, 500e8),
ethPriceFeed,
true
);
uint256 usdPrice = (((100e18 * 1e8) / 500e8) * 2_000e18) / 1e18;
assertEq(usdPrice, 400e18);
assertEq(uint256(priceFeedPool.latestAnswer()), usdPrice);
}

function testPriceFeedPoolReturningPriceWithToken0False() external {
mockPool = new MockPool(new MockERC20("EXA", "EXA", 18), new MockERC20("WETH", "WETH", 18), 500e18, 100e18);
priceFeedPool = new PriceFeedPool(IPool(address(mockPool)), IPriceFeed(address(ethPriceFeed)), false);
(uint256 reserve0, uint256 reserve1, ) = mockPool.getReserves();
uint256 usdPrice = uint256(ethPriceFeed.latestAnswer()).mulDivDown(reserve1, reserve0);
assertEq(usdPrice, 400e18);
assertEq(uint256(priceFeedPool.latestAnswer()), usdPrice);
}

function testPriceFeedPoolWithDifferentDecimalsWithToken0False() external {
mockPool = new MockPool(new MockERC20("EXA", "EXA", 8), new MockERC20("WETH", "WETH", 18), 500e8, 100e18);
priceFeedPool = new PriceFeedPool(IPool(address(mockPool)), IPriceFeed(address(ethPriceFeed)), false);
uint256 usdPrice = (((100e18 * 1e8) / 500e8) * 2_000e18) / 1e18;
assertEq(usdPrice, 400e18);
assertEq(uint256(priceFeedPool.latestAnswer()), usdPrice);
}

function testPriceFeedPoolWithAllDifferentDecimals() external {
priceFeedPool = new PriceFeedPool(
IPool(address(new MockPool(new MockERC20("WETH", "WETH", 8), new MockERC20("EXA", "EXA", 10), 100e8, 500e10))),
IPriceFeed(address(ethPriceFeed)),
true
);
uint256 usdPrice = (((100e8 * 1e10) / 500e10) * 2_000e18) / 1e8;
assertEq(usdPrice, 400e18);
assertEq(uint256(priceFeedPool.latestAnswer()), usdPrice);
}

function testPriceFeedPoolWithAllDifferentDecimalsWithToken0False() external {
mockPool = new MockPool(new MockERC20("EXA", "EXA", 8), new MockERC20("WETH", "WETH", 10), 500e8, 100e10);
priceFeedPool = new PriceFeedPool(IPool(address(mockPool)), IPriceFeed(address(ethPriceFeed)), false);
uint256 usdPrice = (((100e10 * 1e8) / 500e8) * 2_000e18) / 1e10;
assertEq(usdPrice, 400e18);
assertEq(uint256(priceFeedPool.latestAnswer()), usdPrice);
}
}

contract MockPool is IPool {
ERC20 public token0;
ERC20 public token1;
uint256 public reserve0;
uint256 public reserve1;

constructor(ERC20 token0_, ERC20 token1_, uint256 reserve0_, uint256 reserve1_) {
token0 = token0_;
token1 = token1_;
reserve0 = reserve0_;
reserve1 = reserve1_;
}

function getReserves() external view returns (uint256, uint256, uint256) {
return (reserve0, reserve1, 0);
}
}

0 comments on commit 34d960c

Please sign in to comment.