-
Notifications
You must be signed in to change notification settings - Fork 34
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Co-authored-by: danilo neves cruz <cruzdanilo@gmail.com>
- Loading branch information
1 parent
19a99b7
commit 34d960c
Showing
4 changed files
with
158 additions
and
0 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,5 @@ | ||
--- | ||
"@exactly/protocol": patch | ||
--- | ||
|
||
✨ price-feeds: add pool-based feed |
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,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); | ||
} |
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,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); | ||
} | ||
} |