Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions src/interfaces/IPyth.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
// SPDX-License-Identifier: Apache-2.0
pragma solidity >=0.8.15;

/// @title IPyth
/// @notice Minimal interface for Pyth Network price feeds
/// @dev Based on Pyth Network's IPyth interface
interface IPyth {
/// @notice Price struct returned by Pyth Network
/// @param price Price value (multiply by 10^expo to get the decimal value)
/// @param conf Confidence interval, indicating that the actual asset price is ± conf (multiply by 10^expo to get the decimal value)
/// @param expo Exponent
/// @param publishTime Timestamp when price was published
struct Price {
int64 price;
uint64 conf;
int32 expo;
uint256 publishTime;
}

/// @notice Get the price object with a published timestamp from before than `age` seconds in the past
/// @dev Returns the latest price object for the requested price feed ID, if it has been updated sufficiently recently
///
/// @param priceId The ID of the price feed
/// @param age Maximum age of the on-chain price in seconds
/// @return price Price struct containing price, confidence interval, exponent, and publishTime
function getPriceNoOlderThan(
bytes32 priceId,
uint256 age
) external view returns (Price memory price);
}
7 changes: 7 additions & 0 deletions src/libraries/SafeCast.sol
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,13 @@ library SafeCast {
return uint96(amount);
}

function encodeUInt64(uint256 amount) internal pure returns (uint64) {
if (amount > type(uint64).max) {
revert Overflow(amount);
}
return uint64(amount);
}

function encodeUInt48(uint256 amount) internal pure returns (uint48) {
if (amount > type(uint48).max) {
revert Overflow(amount);
Expand Down
446 changes: 446 additions & 0 deletions src/modules/PRICE/submodules/feeds/PythPriceFeeds.sol

Large diffs are not rendered by default.

45 changes: 45 additions & 0 deletions src/test/mocks/MockPyth.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
// SPDX-License-Identifier: MIT
/// forge-lint: disable-start(mixed-case-variable)
pragma solidity ^0.8.0;

import {IPyth} from "interfaces/IPyth.sol";

contract MockPyth is IPyth {
mapping(bytes32 => Price) public s_prices;

// Errors copied from PythErrors.sol

error PriceFeedNotFound();
error StalePrice();

function setPrice(
bytes32 priceId,
int64 price,
uint64 conf,
int32 expo,
uint256 publishTime
) public {
s_prices[priceId] = Price({price: price, conf: conf, expo: expo, publishTime: publishTime});
}

/// @dev Replicates the errors thrown by the Pyth contract
function getPriceNoOlderThan(
bytes32 priceId,
uint256 age
) external view override returns (Price memory) {
Price memory priceData = s_prices[priceId];

// Check if price exists
if (priceData.publishTime == 0) {
revert PriceFeedNotFound();
}

// Check if price is stale
if (priceData.publishTime < block.timestamp - age) {
revert StalePrice();
}

return priceData;
}
}
/// forge-lint: disable-end(mixed-case-variable)
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
// SPDX-License-Identifier: Unlicense
/// forge-lint: disable-start(mixed-case-variable,mixed-case-function)
pragma solidity >=0.8.0;

// Test
import {Test} from "@forge-std-1.9.6/Test.sol";
import {ModuleTestFixtureGenerator} from "src/test/lib/ModuleTestFixtureGenerator.sol";

// Mocks
import {MockPrice} from "src/test/mocks/MockPrice.v2.sol";
import {MockPyth} from "src/test/mocks/MockPyth.sol";

// Libraries
import {FullMath} from "src/libraries/FullMath.sol";

// Bophades
import {Kernel} from "src/Kernel.sol";
import {PythPriceFeeds} from "src/modules/PRICE/submodules/feeds/PythPriceFeeds.sol";

contract PythPriceFeedsTest is Test {
using FullMath for uint256;
using ModuleTestFixtureGenerator for PythPriceFeeds;

MockPyth internal pyth;
MockPyth internal secondPyth;

Kernel internal kernel;
MockPrice internal mockPrice;
PythPriceFeeds internal pythSubmodule;

bytes32 internal constant PRICE_ID_1 = keccak256("PRICE_ID_1");
bytes32 internal constant PRICE_ID_2 = keccak256("PRICE_ID_2");
bytes32 internal constant PRICE_ID_3 = keccak256("PRICE_ID_3");

// Example: price=123456789, expo=-8 represents 1.23456789
int64 internal constant PRICE_1 = 123456789;
int32 internal constant EXPO_1 = -8;
uint64 internal constant CONF_1 = 1000000;
uint256 internal constant EXPECTED_PRICE_1_18_DEC = 1234567890000000000; // 1.23456789 * 10^18

// Example: price=100000000, expo=8 represents 100000000 * 10^8
int64 internal constant PRICE_2 = 100000000;
int32 internal constant EXPO_2 = 8;
uint64 internal constant CONF_2 = 2000000;

// Example: price=500000000, expo=-18 represents 0.0000000005
int64 internal constant PRICE_3 = 500000000;
int32 internal constant EXPO_3 = -18;
uint64 internal constant CONF_3 = 500000;

uint8 internal constant PRICE_DECIMALS = 18;
uint48 internal constant UPDATE_THRESHOLD = 100;
// MAX_CONFIDENCE is in output decimals (18 decimals)
// CONF_1 = 1000000 with expo=-8
// In decimal: 1000000 * 10^-8 = 0.01
// In output decimals (18): 0.01 * 10^18 = 10000000000000000 (1e16)
// Set MAX_CONFIDENCE to 2e16 to allow CONF_1 but reject higher values
uint64 internal constant MAX_CONFIDENCE = 20000000000000000; // 2 * 10^16 in 18 decimals

function setUp() public virtual {
vm.warp(51 * 365 * 24 * 60 * 60); // Set timestamp at roughly Jan 1, 2021

// Set up the Pyth submodule
{
// Deploy kernel
kernel = new Kernel();

// Deploy mockPrice
mockPrice = new MockPrice(kernel, uint8(18), uint32(8 hours));
mockPrice.setTimestamp(uint48(block.timestamp));
mockPrice.setPriceDecimals(PRICE_DECIMALS);

// Deploy Pyth submodule
pythSubmodule = new PythPriceFeeds(mockPrice);
}

// Set up the Pyth contracts
{
pyth = new MockPyth();
secondPyth = new MockPyth();

// With the default update threshold, these prices will NOT be stale

// Set up price feed 1: 1.23456789 (expo=-8)
pyth.setPrice(PRICE_ID_1, PRICE_1, CONF_1, EXPO_1, block.timestamp - UPDATE_THRESHOLD);

// Set up price feed 2: 100000000 * 10^8 (expo=8)
pyth.setPrice(PRICE_ID_2, PRICE_2, CONF_2, EXPO_2, block.timestamp - UPDATE_THRESHOLD);

// Set up price feed 3: 0.0000000005 (expo=-18)
pyth.setPrice(PRICE_ID_3, PRICE_3, CONF_3, EXPO_3, block.timestamp - UPDATE_THRESHOLD);
}
}

// ========= HELPER METHODS ========= //

function encodeOneFeedParams(
address pyth_,
bytes32 priceFeedId_,
uint48 updateThreshold_,
uint256 maxConfidence_
) internal pure returns (bytes memory params) {
return abi.encode(pyth_, priceFeedId_, updateThreshold_, maxConfidence_);
}

function encodeTwoFeedParams(
address firstPyth_,
bytes32 firstPriceFeedId_,
uint48 firstUpdateThreshold_,
uint256 firstMaxConfidence_,
address secondPyth_,
bytes32 secondPriceFeedId_,
uint48 secondUpdateThreshold_,
uint256 secondMaxConfidence_
) internal pure returns (bytes memory params) {
return
abi.encode(
firstPyth_,
firstPriceFeedId_,
firstUpdateThreshold_,
firstMaxConfidence_,
secondPyth_,
secondPriceFeedId_,
secondUpdateThreshold_,
secondMaxConfidence_
);
}
}
/// forge-lint: disable-end(mixed-case-variable,mixed-case-function)
Loading
Loading