Skip to content
Open
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
8 changes: 4 additions & 4 deletions src/generated/FlareFtsoWords.pointers.sol
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ pragma solidity ^0.8.25;
// file needs the contract to exist so that it can be compiled.

/// @dev Hash of the known bytecode.
bytes32 constant BYTECODE_HASH = bytes32(0x69ffc4160d6551b77f501f51e6e53d7825dcd7318519779f877261ebd37b7da0);
bytes32 constant BYTECODE_HASH = bytes32(0x6a26d65b8bf482f013658fbdfe21f4c1df2a439f1b71d1437ad8bc72dd33e225);

/// @dev The hash of the meta that describes the contract.
bytes32 constant DESCRIBED_BY_META_HASH = bytes32(0x4ccb316fd35a2abbee0016f38926a4c95b6270b229c85a9acf8a3a52b36bfedc);
Expand Down Expand Up @@ -48,13 +48,13 @@ bytes constant SUB_PARSER_WORD_PARSERS = hex"093509550967";
/// @dev Every two bytes is a function pointer for an operand handler.
/// These positional indexes all map to the same indexes looked up in the parse
/// meta.
bytes constant OPERAND_HANDLER_FUNCTION_POINTERS = hex"0e430e430e43";
bytes constant OPERAND_HANDLER_FUNCTION_POINTERS = hex"0e040e040e04";

/// @dev The function pointers for the integrity check fns.
bytes constant INTEGRITY_FUNCTION_POINTERS = hex"0e200e2c0e38";
bytes constant INTEGRITY_FUNCTION_POINTERS = hex"0de10ded0df9";

/// @dev The function pointers known to the interpreter for dynamic dispatch.
/// By setting these as a constant they can be inlined into the interpreter
/// and loaded at eval time for very low gas (~100) due to the compiler
/// optimising it to a single `codecopy` to build the in memory bytes array.
bytes constant OPCODE_FUNCTION_POINTERS = hex"0a090ac70b1c";
bytes constant OPCODE_FUNCTION_POINTERS = hex"0a090a880add";
11 changes: 3 additions & 8 deletions src/lib/op/LibOpFtsoCurrentPriceUsd.sol
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ pragma solidity ^0.8.19;
import {OperandV2, StackItem} from "rain-interpreter-interface-0.1.0/src/interface/IInterpreterV4.sol";
import {LibIntOrAString, IntOrAString} from "rain-intorastring-0.1.0/src/lib/LibIntOrAString.sol";
import {LibFtsoCurrentPriceUsd} from "../price/LibFtsoCurrentPriceUsd.sol";
import {DecimalsTooLarge} from "../../err/ErrFtso.sol";

import {LibDecimalFloat, Float} from "rain-math-float-0.1.1/src/lib/LibDecimalFloat.sol";

Expand All @@ -26,9 +25,8 @@ library LibOpFtsoCurrentPriceUsd {
/// address. This registry is used to find the FTSO contract for the symbol
/// and then the price is fetched from the FTSO. The price is returned as a
/// Rain Float, normalized from the FTSO's native decimals.
/// @dev Propagates InactiveFtso, PriceNotFinalized, InconsistentFtso, and
/// StalePrice from LibFtsoCurrentPriceUsd. Additionally reverts with
/// DecimalsTooLarge if the FTSO reports more than 255 decimals.
/// @dev Propagates InactiveFtso, PriceNotFinalized, InconsistentFtso,
/// StalePrice and DecimalsTooLarge from LibFtsoCurrentPriceUsd.
/// @param inputs The inputs to the operation. Always 2 items.
/// 0. The symbol of the asset to fetch the price of, encoded as an
/// unwrapped `IntOrAString` (i.e. a `uint256`).
Expand All @@ -47,10 +45,7 @@ library LibOpFtsoCurrentPriceUsd {
(uint256 price, uint256 decimals) = LibFtsoCurrentPriceUsd.ftsoCurrentPriceUsd(
symbol.toStringV3(), LibDecimalFloat.toFixedDecimalLossless(timeout, 0)
);
if (decimals > type(uint8).max) {
revert DecimalsTooLarge(decimals);
}
// Check above ensures safe downcast.
// Library guarantees decimals <= type(uint8).max; downcast is safe.
//forge-lint: disable-next-line(unsafe-typecast)
Float priceFloat = LibDecimalFloat.fromFixedDecimalLosslessPacked(price, uint8(decimals));

Expand Down
15 changes: 11 additions & 4 deletions src/lib/price/LibFtsoCurrentPriceUsd.sol
Original file line number Diff line number Diff line change
Expand Up @@ -3,26 +3,27 @@
pragma solidity ^0.8.19;

import {IFtsoRegistry, LibFlareContractRegistry} from "../registry/LibFlareContractRegistry.sol";
import {InactiveFtso, PriceNotFinalized, StalePrice, InconsistentFtso} from "../../err/ErrFtso.sol";
import {InactiveFtso, PriceNotFinalized, StalePrice, InconsistentFtso, DecimalsTooLarge} from "../../err/ErrFtso.sol";
import {IFtso} from "../../vendor/flare-smart-contracts/userInterfaces/IFtso.sol";

library LibFtsoCurrentPriceUsd {
/// @notice Fetches the current FTSO USD price for a symbol and returns the
/// raw price together with its native decimal count. The caller is
/// responsible for normalising to 18 decimals and enforcing the
/// DecimalsTooLarge guard.
/// responsible for normalising to 18 decimals.
/// @dev Reverts with InactiveFtso if the FTSO is not active.
/// Reverts with PriceNotFinalized if the finalization type is not
/// WEIGHTED_MEDIAN or TRUSTED_ADDRESSES.
/// Reverts with InconsistentFtso if the two price reads return different
/// values (indicates an FTSO bug).
/// Reverts with StalePrice(priceTimestamp, timeout) if the price is older
/// than timeout seconds.
/// Reverts with DecimalsTooLarge(decimals) if the FTSO reports more
/// decimals than fit in a uint8.
/// @param symbol The FTSO symbol string (e.g. "FLR", "ETH").
/// @param timeout Max age in seconds; prices older than this revert.
/// @return price The raw FTSO price in the FTSO's native unit.
/// @return decimals The decimal precision of the returned price (typically
/// 5 for Flare FTSO v1, but not guaranteed).
/// 5 for Flare FTSO v1, but not guaranteed). Always <= type(uint8).max.
function ftsoCurrentPriceUsd(string memory symbol, uint256 timeout) internal view returns (uint256, uint256) {
// Fetch the FTSO from the registry.
IFtsoRegistry ftsoRegistry = LibFlareContractRegistry.getFtsoRegistry();
Expand Down Expand Up @@ -67,6 +68,12 @@ library LibFtsoCurrentPriceUsd {
revert StalePrice(priceTimestamp, timeout);
}

// The FTSO is untrusted so the decimals bound is enforced here, at the
// trust boundary, rather than at each call site.
if (decimals > type(uint8).max) {
revert DecimalsTooLarge(decimals);
}

return (price, decimals);
}
}
1 change: 1 addition & 0 deletions src/lib/sflr/LibSceptreStakedFlare.sol
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ library LibSceptreStakedFlare {
/// Fixed 18 decimal place ratio of sFLR to FLR.
/// For each 1e18 FLR pooled, this is how many sFLR shares it is worth
/// according to the sFLR contract's current exchange rate.
/// Reverts with ZeroSFLRRate if the sFLR contract returns 0.
//forge-lint: disable-next-line(mixed-case-function)
function getSFLRPerFLR18() internal view returns (uint256) {
uint256 rate = SFLR_CONTRACT.getSharesByPooledFlr(1e18);
Expand Down
166 changes: 166 additions & 0 deletions test/src/lib/price/LibFtsoCurrentPriceUsd.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
// SPDX-License-Identifier: LicenseRef-DCL-1.0
// SPDX-FileCopyrightText: Copyright (c) 2020 Rain Open Source Software Ltd
pragma solidity =0.8.25;

import {FtsoTest, OperandV2, StackItem} from "../../../abstract/FtsoTest.sol";
import {LibFtsoCurrentPriceUsd} from "src/lib/price/LibFtsoCurrentPriceUsd.sol";
import {DecimalsTooLarge, StalePrice} from "src/err/ErrFtso.sol";
import {LibIntOrAString, IntOrAString} from "rain-intorastring-0.1.0/src/lib/LibIntOrAString.sol";

/// @title LibFtsoCurrentPriceUsdTest
/// @notice Exercises `LibFtsoCurrentPriceUsd.ftsoCurrentPriceUsd` DIRECTLY,
/// without going through `LibOpFtsoCurrentPriceUsd`. The FTSO is untrusted, so
/// the bounds it enforces on FTSO-reported values MUST hold for every caller of
/// the library, not just the one op that happens to call it today.
contract LibFtsoCurrentPriceUsdTest is FtsoTest {
using LibIntOrAString for IntOrAString;

/// External wrapper so `vm.expectRevert` monitors the correct call frame.
/// The library function is `internal`, so an inline call would revert in the
/// test frame itself rather than in a call `vm.expectRevert` intercepts.
function externalFtsoCurrentPriceUsd(string memory symbol, uint256 timeout)
external
view
returns (uint256, uint256)
{
return LibFtsoCurrentPriceUsd.ftsoCurrentPriceUsd(symbol, timeout);
}

/// Satisfies `FtsoTest` so the inherited registry-failure tests exercise the
/// library directly. Outputs are `[price, decimals]`.
function externalRun(OperandV2, StackItem[] memory inputs) external view override returns (StackItem[] memory) {
IntOrAString symbol;
uint256 timeout;
assembly ("memory-safe") {
symbol := mload(add(inputs, 0x20))
timeout := mload(add(inputs, 0x40))
}
(uint256 price, uint256 decimals) = LibFtsoCurrentPriceUsd.ftsoCurrentPriceUsd(symbol.toStringV3(), timeout);
StackItem[] memory outputs = new StackItem[](2);
outputs[0] = StackItem.wrap(bytes32(price));
outputs[1] = StackItem.wrap(bytes32(decimals));
return outputs;
}

/// Mocks a well formed, finalized, non-stale FTSO reporting `currentPrice`.
function mockHealthyFtso(string memory symbol, PriceDetails memory priceDetails, CurrentPrice memory currentPrice)
internal
{
conformPriceDetails(priceDetails, currentPrice);
finalizePrice(priceDetails);

mockRegistry();
mockFtsoRegistry(FTSO, symbol);
activateFtso();
mockPriceDetails(priceDetails);
mockPrice(FTSO, currentPrice);
}

/// In bound decimals are returned to the caller unchanged, alongside the raw
/// price. This is the oracle the guard is protecting: the library never
/// rescales, it only bounds.
function testFtsoCurrentPriceUsdHappy(
string memory symbol,
uint256 timeout,
uint256 currentTime,
PriceDetails memory priceDetails,
CurrentPrice memory currentPrice
) external {
currentPrice.price = bound(currentPrice.price, 0, uint256(int256(type(int224).max)));
currentPrice.decimals = bound(currentPrice.decimals, 0, type(uint8).max);
timeout = bound(timeout, 0, uint256(int256(type(int224).max)));
currentTime = warpNotStale(currentPrice, timeout, currentTime);

mockHealthyFtso(symbol, priceDetails, currentPrice);

(uint256 price, uint256 decimals) = this.externalFtsoCurrentPriceUsd(symbol, timeout);
assertEq(price, currentPrice.price, "price");
assertEq(decimals, currentPrice.decimals, "decimals");
}

/// An FTSO reporting more decimals than fit in a uint8 MUST be rejected by
/// the library itself, with the offending value in the error data. Without
/// this bound a direct caller would silently downcast to `uint8` and
/// mis-scale the price by orders of magnitude.
function testFtsoCurrentPriceUsdDecimalsTooLargeReverts(
string memory symbol,
uint256 timeout,
uint256 currentTime,
PriceDetails memory priceDetails,
CurrentPrice memory currentPrice
) external {
currentPrice.price = bound(currentPrice.price, 0, uint256(int256(type(int224).max)));
currentPrice.decimals =
bound(currentPrice.decimals, uint256(type(uint8).max) + 1, uint256(int256(type(int32).max)));
timeout = bound(timeout, 0, uint256(int256(type(int224).max)));
currentTime = warpNotStale(currentPrice, timeout, currentTime);

mockHealthyFtso(symbol, priceDetails, currentPrice);

vm.expectRevert(abi.encodeWithSelector(DecimalsTooLarge.selector, currentPrice.decimals));
this.externalFtsoCurrentPriceUsd(symbol, timeout);
}

/// The exact boundary, low side: `type(uint8).max` decimals is the largest
/// value that survives the downcast, so it MUST be accepted and returned
/// verbatim. Pins the `>` in the guard against being weakened to `>=`.
function testFtsoCurrentPriceUsdDecimalsBoundaryMaxAccepted() external {
string memory symbol = "ETH";
uint256 timeout = 3600;

CurrentPrice memory currentPrice;
currentPrice.price = 98765;
currentPrice.decimals = uint256(type(uint8).max);
currentPrice.timestamp = 50000;
vm.warp(currentPrice.timestamp);

PriceDetails memory priceDetails;
mockHealthyFtso(symbol, priceDetails, currentPrice);

(uint256 price, uint256 decimals) = this.externalFtsoCurrentPriceUsd(symbol, timeout);
assertEq(price, 98765, "price");
assertEq(decimals, uint256(type(uint8).max), "decimals");
}

/// The exact boundary, high side: one more than `type(uint8).max` MUST
/// revert, carrying the unclamped value. Pins the `>` in the guard against
/// being widened to `type(uint8).max + 1` or dropped entirely.
function testFtsoCurrentPriceUsdDecimalsBoundaryOverMaxReverts() external {
string memory symbol = "ETH";
uint256 timeout = 3600;

CurrentPrice memory currentPrice;
currentPrice.price = 98765;
currentPrice.decimals = uint256(type(uint8).max) + 1;
currentPrice.timestamp = 50000;
vm.warp(currentPrice.timestamp);

PriceDetails memory priceDetails;
mockHealthyFtso(symbol, priceDetails, currentPrice);

vm.expectRevert(abi.encodeWithSelector(DecimalsTooLarge.selector, uint256(type(uint8).max) + 1));
this.externalFtsoCurrentPriceUsd(symbol, timeout);
}

/// Staleness is checked before the decimals bound, so a stale price with
/// oversized decimals reports StalePrice rather than DecimalsTooLarge. Pins
/// the guard's position at the end of the checks, where it cannot mask an
/// earlier failure.
function testFtsoCurrentPriceUsdStaleTakesPrecedenceOverDecimals() external {
string memory symbol = "ETH";
uint256 timeout = 3600;

CurrentPrice memory currentPrice;
currentPrice.price = 98765;
currentPrice.decimals = uint256(type(uint8).max) + 1;
currentPrice.timestamp = 50000;
// One second past the staleness boundary.
vm.warp(currentPrice.timestamp + timeout + 1);

PriceDetails memory priceDetails;
mockHealthyFtso(symbol, priceDetails, currentPrice);

vm.expectRevert(abi.encodeWithSelector(StalePrice.selector, currentPrice.timestamp, timeout));
this.externalFtsoCurrentPriceUsd(symbol, timeout);
}
}
Loading