Skip to content

Commit

Permalink
feat: PriceRegistry conforms to keystone interface (#1208)
Browse files Browse the repository at this point in the history
Depends on : smartcontractkit/chainlink#13878

## Motivation
The price registry needs to conform to the KeystoneFeedsConsumer
interface in order to receive keystone price feed updates.

## Solution

Implemented Keystones `IReciever` interface `onReport` function to
handle the following report type

```
struct ReceivedFeedReport {
    address Token;
    uint224 Price;
    uint32 Timestamp;
}
```

and storing the reported fee in `Internal.TimestampedPackedUint224` for
`s_usdPerToken` mapping

---------

Signed-off-by: 0xsuryansh <[email protected]>
  • Loading branch information
0xsuryansh authored Aug 22, 2024
1 parent d6a5f16 commit 8ddbe66
Show file tree
Hide file tree
Showing 10 changed files with 995 additions and 248 deletions.
318 changes: 161 additions & 157 deletions contracts/gas-snapshots/ccip.gas-snapshot

Large diffs are not rendered by default.

109 changes: 86 additions & 23 deletions contracts/src/v0.8/ccip/PriceRegistry.sol
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,24 @@ import {Internal} from "./libraries/Internal.sol";
import {Pool} from "./libraries/Pool.sol";
import {USDPriceWith18Decimals} from "./libraries/USDPriceWith18Decimals.sol";

import {KeystoneFeedsPermissionHandler} from "../keystone/KeystoneFeedsPermissionHandler.sol";
import {IReceiver} from "../keystone/interfaces/IReceiver.sol";
import {KeystoneFeedDefaultMetadataLib} from "../keystone/lib/KeystoneFeedDefaultMetadataLib.sol";
import {EnumerableSet} from "../vendor/openzeppelin-solidity/v5.0.2/contracts/utils/structs/EnumerableSet.sol";

/// @notice The PriceRegistry contract responsibility is to store the current gas price in USD for a given destination chain,
/// and the price of a token in USD allowing the owner or priceUpdater to update this value.
/// The authorized callers in the contract represent the fee price updaters.
contract PriceRegistry is AuthorizedCallers, IPriceRegistry, ITypeAndVersion {
contract PriceRegistry is
AuthorizedCallers,
IPriceRegistry,
ITypeAndVersion,
IReceiver,
KeystoneFeedsPermissionHandler
{
using EnumerableSet for EnumerableSet.AddressSet;
using USDPriceWith18Decimals for uint224;
using KeystoneFeedDefaultMetadataLib for bytes;

/// @notice Token price data feed update
struct TokenPriceFeedUpdate {
Expand All @@ -35,9 +45,17 @@ contract PriceRegistry is AuthorizedCallers, IPriceRegistry, ITypeAndVersion {
uint32 stalenessThreshold; // The amount of time a gas price can be stale before it is considered invalid.
}

/// @notice The struct representing the received CCIP feed report from keystone IReceiver.onReport()
struct ReceivedCCIPFeedReport {
address token; // Token address
uint224 price; // ─────────╮ Price of the token in USD with 18 decimals
uint32 timestamp; // ──────╯ Timestamp of the price update
}

error TokenNotSupported(address token);
error ChainNotSupported(uint64 chain);
error StaleGasPrice(uint64 destChainSelector, uint256 threshold, uint256 timePassed);
error StaleKeystoneUpdate(address token, uint256 feedTimestamp, uint256 storedTimeStamp);
error DataFeedValueOutOfUint224Range();
error InvalidDestBytesOverhead(address token, uint32 destBytesOverhead);
error MessageGasLimitTooHigh();
Expand Down Expand Up @@ -325,30 +343,11 @@ contract PriceRegistry is AuthorizedCallers, IPriceRegistry, ITypeAndVersion {
if (dataFeedAnswer < 0) {
revert DataFeedValueOutOfUint224Range();
}
uint256 rebasedValue = uint256(dataFeedAnswer);

// Rebase formula for units in smallest token denomination: usdValue * (1e18 * 1e18) / 1eTokenDecimals
// feedValue * (10 ** (18 - feedDecimals)) * (10 ** (18 - erc20Decimals))
// feedValue * (10 ** ((18 - feedDecimals) + (18 - erc20Decimals)))
// feedValue * (10 ** (36 - feedDecimals - erc20Decimals))
// feedValue * (10 ** (36 - (feedDecimals + erc20Decimals)))
// feedValue * (10 ** (36 - excessDecimals))
// If excessDecimals > 36 => flip it to feedValue / (10 ** (excessDecimals - 36))

uint8 excessDecimals = dataFeedContract.decimals() + priceFeedConfig.tokenDecimals;

if (excessDecimals > 36) {
rebasedValue /= 10 ** (excessDecimals - 36);
} else {
rebasedValue *= 10 ** (36 - excessDecimals);
}

if (rebasedValue > type(uint224).max) {
revert DataFeedValueOutOfUint224Range();
}
uint224 rebasedValue =
_calculateRebasedValue(dataFeedContract.decimals(), priceFeedConfig.tokenDecimals, uint256(dataFeedAnswer));

// Data feed staleness is unchecked to decouple the PriceRegistry from data feed delay issues
return Internal.TimestampedPackedUint224({value: uint224(rebasedValue), timestamp: uint32(block.timestamp)});
return Internal.TimestampedPackedUint224({value: rebasedValue, timestamp: uint32(block.timestamp)});
}

// ================================================================
Expand Down Expand Up @@ -435,6 +434,37 @@ contract PriceRegistry is AuthorizedCallers, IPriceRegistry, ITypeAndVersion {
}
}

/// @notice Handles the report containing price feeds and updates the internal price storage
/// @inheritdoc IReceiver
/// @dev This function is called to process incoming price feed data.
/// @param metadata Arbitrary metadata associated with the report (not used in this implementation).
/// @param report Encoded report containing an array of `ReceivedCCIPFeedReport` structs.
function onReport(bytes calldata metadata, bytes calldata report) external {
(bytes10 workflowName, address workflowOwner, bytes2 reportName) = metadata._extractMetadataInfo();

_validateReportPermission(msg.sender, workflowOwner, workflowName, reportName);

ReceivedCCIPFeedReport[] memory feeds = abi.decode(report, (ReceivedCCIPFeedReport[]));

for (uint256 i = 0; i < feeds.length; ++i) {
uint8 tokenDecimals = s_usdPriceFeedsPerToken[feeds[i].token].tokenDecimals;
if (tokenDecimals == 0) {
revert TokenNotSupported(feeds[i].token);
}
// Keystone reports prices in USD with 18 decimals, so we passing it as 18 in the _calculateRebasedValue function
uint224 rebasedValue = _calculateRebasedValue(18, tokenDecimals, feeds[i].price);

//if stale update then revert
if (feeds[i].timestamp < s_usdPerToken[feeds[i].token].timestamp) {
revert StaleKeystoneUpdate(feeds[i].token, feeds[i].timestamp, s_usdPerToken[feeds[i].token].timestamp);
}

s_usdPerToken[feeds[i].token] =
Internal.TimestampedPackedUint224({value: rebasedValue, timestamp: feeds[i].timestamp});
emit UsdPerTokenUpdated(feeds[i].token, rebasedValue, feeds[i].timestamp);
}
}

// ================================================================
// │ Fee quoting │
// ================================================================
Expand Down Expand Up @@ -612,6 +642,39 @@ contract PriceRegistry is AuthorizedCallers, IPriceRegistry, ITypeAndVersion {
return (tokenTransferFeeUSDWei, tokenTransferGas, tokenTransferBytesOverhead);
}

/// @notice calculates the rebased value for 1e18 smallest token denomination
/// @param dataFeedDecimal decimal of the data feed
/// @param tokenDecimal decimal of the token
/// @param feedValue value of the data feed
/// @return rebasedValue rebased value
function _calculateRebasedValue(
uint8 dataFeedDecimal,
uint8 tokenDecimal,
uint256 feedValue
) internal pure returns (uint224 rebasedValue) {
// Rebase formula for units in smallest token denomination: usdValue * (1e18 * 1e18) / 1eTokenDecimals
// feedValue * (10 ** (18 - feedDecimals)) * (10 ** (18 - erc20Decimals))
// feedValue * (10 ** ((18 - feedDecimals) + (18 - erc20Decimals)))
// feedValue * (10 ** (36 - feedDecimals - erc20Decimals))
// feedValue * (10 ** (36 - (feedDecimals + erc20Decimals)))
// feedValue * (10 ** (36 - excessDecimals))
// If excessDecimals > 36 => flip it to feedValue / (10 ** (excessDecimals - 36))
uint8 excessDecimals = dataFeedDecimal + tokenDecimal;
uint256 rebasedVal;

if (excessDecimals > 36) {
rebasedVal = feedValue / (10 ** (excessDecimals - 36));
} else {
rebasedVal = feedValue * (10 ** (36 - excessDecimals));
}

if (rebasedVal > type(uint224).max) {
revert DataFeedValueOutOfUint224Range();
}

return uint224(rebasedVal);
}

/// @notice Returns the estimated data availability cost of the message.
/// @dev To save on gas, we use a single destGasPerDataAvailabilityByte value for both zero and non-zero bytes.
/// @param destChainConfig the config configured for the destination chain selector.
Expand Down
8 changes: 8 additions & 0 deletions contracts/src/v0.8/ccip/test/helpers/PriceRegistryHelper.sol
Original file line number Diff line number Diff line change
Expand Up @@ -69,4 +69,12 @@ contract PriceRegistryHelper is PriceRegistry {
function validateDestFamilyAddress(bytes4 chainFamilySelector, bytes memory destAddress) external pure {
_validateDestFamilyAddress(chainFamilySelector, destAddress);
}

function calculateRebasedValue(
uint8 dataFeedDecimal,
uint8 tokenDecimal,
uint256 feedValue
) external pure returns (uint224) {
return _calculateRebasedValue(dataFeedDecimal, tokenDecimal, feedValue);
}
}
138 changes: 138 additions & 0 deletions contracts/src/v0.8/ccip/test/priceRegistry/PriceRegistry.t.sol
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
// SPDX-License-Identifier: BUSL-1.1
pragma solidity 0.8.24;

import {KeystoneFeedsPermissionHandler} from "../../../keystone/KeystoneFeedsPermissionHandler.sol";
import {AuthorizedCallers} from "../../../shared/access/AuthorizedCallers.sol";
import {MockV3Aggregator} from "../../../tests/MockV3Aggregator.sol";
import {PriceRegistry} from "../../PriceRegistry.sol";
import {IPriceRegistry} from "../../interfaces/IPriceRegistry.sol";
import {Client} from "../../libraries/Client.sol";
import {Internal} from "../../libraries/Internal.sol";
import {Pool} from "../../libraries/Pool.sol";
Expand Down Expand Up @@ -2089,3 +2091,139 @@ contract PriceRegistry_parseEVMExtraArgsFromBytes is PriceRegistrySetup {
s_priceRegistry.parseEVMExtraArgsFromBytes(inputExtraArgs, s_destChainConfig);
}
}

contract PriceRegistry_KeystoneSetup is PriceRegistrySetup {
address constant FORWARDER_1 = address(0x1);
address constant WORKFLOW_OWNER_1 = address(0x3);
bytes10 constant WORKFLOW_NAME_1 = "workflow1";
bytes2 constant REPORT_NAME_1 = "01";
address onReportTestToken1;
address onReportTestToken2;

function setUp() public virtual override {
super.setUp();
onReportTestToken1 = s_sourceTokens[0];
onReportTestToken2 = _deploySourceToken("onReportTestToken2", 0, 20);

KeystoneFeedsPermissionHandler.Permission[] memory permissions = new KeystoneFeedsPermissionHandler.Permission[](1);
permissions[0] = KeystoneFeedsPermissionHandler.Permission({
forwarder: FORWARDER_1,
workflowOwner: WORKFLOW_OWNER_1,
workflowName: WORKFLOW_NAME_1,
reportName: REPORT_NAME_1,
isAllowed: true
});
PriceRegistry.TokenPriceFeedUpdate[] memory tokenPriceFeeds = new PriceRegistry.TokenPriceFeedUpdate[](2);
tokenPriceFeeds[0] = PriceRegistry.TokenPriceFeedUpdate({
sourceToken: onReportTestToken1,
feedConfig: IPriceRegistry.TokenPriceFeedConfig({dataFeedAddress: address(0x0), tokenDecimals: 18})
});
tokenPriceFeeds[1] = PriceRegistry.TokenPriceFeedUpdate({
sourceToken: onReportTestToken2,
feedConfig: IPriceRegistry.TokenPriceFeedConfig({dataFeedAddress: address(0x0), tokenDecimals: 20})
});
s_priceRegistry.setReportPermissions(permissions);
s_priceRegistry.updateTokenPriceFeeds(tokenPriceFeeds);
}
}

contract PriceRegistry_onReport is PriceRegistry_KeystoneSetup {
function test_onReport_Success() public {
bytes memory encodedPermissionsMetadata =
abi.encodePacked(keccak256(abi.encode("workflowCID")), WORKFLOW_NAME_1, WORKFLOW_OWNER_1, REPORT_NAME_1);

PriceRegistry.ReceivedCCIPFeedReport[] memory report = new PriceRegistry.ReceivedCCIPFeedReport[](2);
report[0] =
PriceRegistry.ReceivedCCIPFeedReport({token: onReportTestToken1, price: 4e18, timestamp: uint32(block.timestamp)});
report[1] =
PriceRegistry.ReceivedCCIPFeedReport({token: onReportTestToken2, price: 4e18, timestamp: uint32(block.timestamp)});

bytes memory encodedReport = abi.encode(report);
uint224 expectedStoredToken1Price = s_priceRegistry.calculateRebasedValue(18, 18, report[0].price);
uint224 expectedStoredToken2Price = s_priceRegistry.calculateRebasedValue(18, 20, report[1].price);
vm.expectEmit();
emit PriceRegistry.UsdPerTokenUpdated(onReportTestToken1, expectedStoredToken1Price, block.timestamp);
vm.expectEmit();
emit PriceRegistry.UsdPerTokenUpdated(onReportTestToken2, expectedStoredToken2Price, block.timestamp);

changePrank(FORWARDER_1);
s_priceRegistry.onReport(encodedPermissionsMetadata, encodedReport);

vm.assertEq(s_priceRegistry.getTokenPrice(report[0].token).value, expectedStoredToken1Price);
vm.assertEq(s_priceRegistry.getTokenPrice(report[0].token).timestamp, report[0].timestamp);

vm.assertEq(s_priceRegistry.getTokenPrice(report[1].token).value, expectedStoredToken2Price);
vm.assertEq(s_priceRegistry.getTokenPrice(report[1].token).timestamp, report[1].timestamp);
}

function test_onReport_InvalidForwarder_Reverts() public {
bytes memory encodedPermissionsMetadata =
abi.encodePacked(keccak256(abi.encode("workflowCID")), WORKFLOW_NAME_1, WORKFLOW_OWNER_1, REPORT_NAME_1);
PriceRegistry.ReceivedCCIPFeedReport[] memory report = new PriceRegistry.ReceivedCCIPFeedReport[](1);
report[0] =
PriceRegistry.ReceivedCCIPFeedReport({token: s_sourceTokens[0], price: 4e18, timestamp: uint32(block.timestamp)});

bytes memory encodedReport = abi.encode(report);

vm.expectRevert(
abi.encodeWithSelector(
KeystoneFeedsPermissionHandler.ReportForwarderUnauthorized.selector,
STRANGER,
WORKFLOW_OWNER_1,
WORKFLOW_NAME_1,
REPORT_NAME_1
)
);
changePrank(STRANGER);
s_priceRegistry.onReport(encodedPermissionsMetadata, encodedReport);
}

function test_onReport_UnsupportedToken_Reverts() public {
bytes memory encodedPermissionsMetadata =
abi.encodePacked(keccak256(abi.encode("workflowCID")), WORKFLOW_NAME_1, WORKFLOW_OWNER_1, REPORT_NAME_1);
PriceRegistry.ReceivedCCIPFeedReport[] memory report = new PriceRegistry.ReceivedCCIPFeedReport[](1);
report[0] =
PriceRegistry.ReceivedCCIPFeedReport({token: s_sourceTokens[1], price: 4e18, timestamp: uint32(block.timestamp)});

bytes memory encodedReport = abi.encode(report);

vm.expectRevert(abi.encodeWithSelector(PriceRegistry.TokenNotSupported.selector, s_sourceTokens[1]));
changePrank(FORWARDER_1);
s_priceRegistry.onReport(encodedPermissionsMetadata, encodedReport);
}

function test_OnReport_StaleUpdate_Revert() public {
//Creating a correct report
bytes memory encodedPermissionsMetadata =
abi.encodePacked(keccak256(abi.encode("workflowCID")), WORKFLOW_NAME_1, WORKFLOW_OWNER_1, REPORT_NAME_1);

PriceRegistry.ReceivedCCIPFeedReport[] memory report = new PriceRegistry.ReceivedCCIPFeedReport[](1);
report[0] =
PriceRegistry.ReceivedCCIPFeedReport({token: onReportTestToken1, price: 4e18, timestamp: uint32(block.timestamp)});

bytes memory encodedReport = abi.encode(report);
uint224 expectedStoredTokenPrice = s_priceRegistry.calculateRebasedValue(18, 18, report[0].price);

vm.expectEmit();
emit PriceRegistry.UsdPerTokenUpdated(onReportTestToken1, expectedStoredTokenPrice, block.timestamp);

changePrank(FORWARDER_1);
//setting the correct price and time with the correct report
s_priceRegistry.onReport(encodedPermissionsMetadata, encodedReport);

//create a stale report
report[0] = PriceRegistry.ReceivedCCIPFeedReport({
token: onReportTestToken1,
price: 4e18,
timestamp: uint32(block.timestamp - 1)
});
encodedReport = abi.encode(report);
//expecting a revert
vm.expectRevert(
abi.encodeWithSelector(
PriceRegistry.StaleKeystoneUpdate.selector, onReportTestToken1, block.timestamp - 1, block.timestamp
)
);
s_priceRegistry.onReport(encodedPermissionsMetadata, encodedReport);
}
}
Loading

0 comments on commit 8ddbe66

Please sign in to comment.