diff --git a/src/oracles/ChainlinkToOracleAdapter.sol b/src/oracles/ChainlinkToOracleAdapter.sol index e471284..f64f26e 100644 --- a/src/oracles/ChainlinkToOracleAdapter.sol +++ b/src/oracles/ChainlinkToOracleAdapter.sol @@ -2,6 +2,7 @@ pragma solidity ^0.8.20; import {IOracle} from "../interfaces/IOracle.sol"; +import {OracleScalingLib} from "./OracleScalingLib.sol"; // minimal chainlink interface, only what we need interface AggregatorV3Interface { @@ -29,7 +30,7 @@ contract ChainlinkToOracleAdapter is IOracle { require(answer > 0, "bad value"); // casting is safe because answer > 0 // forge-lint: disable-next-line(unsafe-typecast) - return _scaleToWad(uint256(answer), feed.decimals()); + return OracleScalingLib.scaleToWad(uint256(answer), feed.decimals()); } // chainlink has single value, so min = max = value @@ -50,15 +51,4 @@ contract ChainlinkToOracleAdapter is IOracle { function description() external view returns (string memory) { return feed.description(); } - - // scale native decimals to WAD (1e18) - function _scaleToWad(uint256 value, uint8 valueDecimals) internal pure returns (uint256) { - if (valueDecimals == 18) { - return value; - } - if (valueDecimals < 18) { - return value * (10 ** (18 - uint256(valueDecimals))); - } - return value / (10 ** (uint256(valueDecimals) - 18)); - } } diff --git a/src/oracles/OracleScalingLib.sol b/src/oracles/OracleScalingLib.sol new file mode 100644 index 0000000..7cb2767 --- /dev/null +++ b/src/oracles/OracleScalingLib.sol @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +library OracleScalingLib { + function scaleToWad(uint256 value, uint8 valueDecimals) internal pure returns (uint256) { + if (valueDecimals == 18) { + return value; + } + if (valueDecimals < 18) { + return value * (10 ** (18 - uint256(valueDecimals))); + } + return value / (10 ** (uint256(valueDecimals) - 18)); + } +} diff --git a/src/oracles/OrbOracleToOracleAdapter.sol b/src/oracles/OrbOracleToOracleAdapter.sol new file mode 100644 index 0000000..08e683e --- /dev/null +++ b/src/oracles/OrbOracleToOracleAdapter.sol @@ -0,0 +1,54 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import {IOracle} from "../interfaces/IOracle.sol"; +import {OracleScalingLib} from "./OracleScalingLib.sol"; + +interface OrbOracleInterface { + function readValue() external view returns (int256); + function readMaxValue(uint256 sampleSize) external view returns (int256); + function readMinValue(uint256 sampleSize) external view returns (int256); + function lastSubmissionTime() external view returns (uint256); + function description() external view returns (string memory); +} + +contract OrbOracleToOracleAdapter is IOracle { + OrbOracleInterface public immutable feed; + uint8 public immutable valueDecimals; + uint256 public immutable sampleSize; + + constructor(address feedParam, uint8 valueDecimalsParam, uint256 sampleSizeParam) { + require(feedParam != address(0), "invalid feed"); + require(sampleSizeParam > 0, "invalid sample size"); + + feed = OrbOracleInterface(feedParam); + valueDecimals = valueDecimalsParam; + sampleSize = sampleSizeParam; + } + + function readValue() public view returns (uint256 value) { + return _scaleIntToWad(feed.readValue()); + } + + function readMaxValue() external view returns (uint256 maxValue) { + return _scaleIntToWad(feed.readMaxValue(sampleSize)); + } + + function readMinValue() external view returns (uint256 minValue) { + return _scaleIntToWad(feed.readMinValue(sampleSize)); + } + + function lastUpdated() external view returns (uint256 timestamp) { + return feed.lastSubmissionTime(); + } + + function description() external view returns (string memory) { + return feed.description(); + } + + function _scaleIntToWad(int256 value) internal view returns (uint256) { + require(value > 0, "bad value"); + // forge-lint: disable-next-line(unsafe-typecast) + return OracleScalingLib.scaleToWad(uint256(value), valueDecimals); + } +} diff --git a/test/OrbOracleAdapter.t.sol b/test/OrbOracleAdapter.t.sol new file mode 100644 index 0000000..9c467ea --- /dev/null +++ b/test/OrbOracleAdapter.t.sol @@ -0,0 +1,125 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import {Test} from "forge-std/Test.sol"; +import {OrbOracleToOracleAdapter} from "../src/oracles/OrbOracleToOracleAdapter.sol"; + +contract MockOrbOracle { + int256 public valueVal; + int256 public maxVal; + int256 public minVal; + uint256 public lastSubmissionTimeVal; + uint256 public expectedSampleSize; + string private descriptionVal; + + constructor(int256 value_, int256 max_, int256 min_, uint256 sampleSize_, string memory description_) { + valueVal = value_; + maxVal = max_; + minVal = min_; + expectedSampleSize = sampleSize_; + descriptionVal = description_; + lastSubmissionTimeVal = block.timestamp; + } + + function readValue() external view returns (int256) { + return valueVal; + } + + function readMaxValue(uint256 sampleSize) external view returns (int256) { + require(sampleSize == expectedSampleSize, "wrong sample size"); + return maxVal; + } + + function readMinValue(uint256 sampleSize) external view returns (int256) { + require(sampleSize == expectedSampleSize, "wrong sample size"); + return minVal; + } + + function lastSubmissionTime() external view returns (uint256) { + return lastSubmissionTimeVal; + } + + function description() external view returns (string memory) { + return descriptionVal; + } +} + +contract OrbOracleAdapterTest is Test { + function testScalesEightDecimalsToWad() public { + MockOrbOracle feed = new MockOrbOracle(3000 * 1e8, 3100 * 1e8, 2900 * 1e8, 3, "ORB / USD"); + OrbOracleToOracleAdapter adapter = new OrbOracleToOracleAdapter(address(feed), 8, 3); + + assertEq(adapter.readValue(), 3000 * 1e18, "8 dec scaling failed"); + } + + function testSupportsEighteenDecimals() public { + MockOrbOracle feed = new MockOrbOracle(2000 * 1e18, 2100 * 1e18, 1900 * 1e18, 3, "ORB / USD"); + OrbOracleToOracleAdapter adapter = new OrbOracleToOracleAdapter(address(feed), 18, 3); + + assertEq(adapter.readValue(), 2000 * 1e18, "18 dec scaling failed"); + } + + function testSupportsMoreThanEighteenDecimals() public { + MockOrbOracle feed = new MockOrbOracle(2000 * 1e20, 2100 * 1e20, 1900 * 1e20, 3, "ORB / USD"); + OrbOracleToOracleAdapter adapter = new OrbOracleToOracleAdapter(address(feed), 20, 3); + + assertEq(adapter.readValue(), 2000 * 1e18, "20 dec scaling failed"); + } + + function testMinAndMaxUseSampleSize() public { + MockOrbOracle feed = new MockOrbOracle(3000 * 1e8, 3200 * 1e8, 2800 * 1e8, 5, "ORB / USD"); + OrbOracleToOracleAdapter adapter = new OrbOracleToOracleAdapter(address(feed), 8, 5); + + assertEq(adapter.readMaxValue(), 3200 * 1e18, "wrong max value"); + assertEq(adapter.readMinValue(), 2800 * 1e18, "wrong min value"); + } + + function testLastUpdatedReturnsSubmissionTime() public { + MockOrbOracle feed = new MockOrbOracle(100 * 1e8, 110 * 1e8, 90 * 1e8, 3, "ORB / USD"); + OrbOracleToOracleAdapter adapter = new OrbOracleToOracleAdapter(address(feed), 8, 3); + + assertEq(adapter.lastUpdated(), block.timestamp, "wrong timestamp"); + } + + function testDescriptionReturnsFeedDescription() public { + MockOrbOracle feed = new MockOrbOracle(100 * 1e8, 110 * 1e8, 90 * 1e8, 3, "ORB / USD"); + OrbOracleToOracleAdapter adapter = new OrbOracleToOracleAdapter(address(feed), 8, 3); + + assertEq(adapter.description(), "ORB / USD", "wrong description"); + } + + function testRevertsOnZeroValue() public { + MockOrbOracle feed = new MockOrbOracle(0, 110 * 1e8, 90 * 1e8, 3, "ORB / USD"); + OrbOracleToOracleAdapter adapter = new OrbOracleToOracleAdapter(address(feed), 8, 3); + + vm.expectRevert("bad value"); + adapter.readValue(); + } + + function testRevertsOnNegativeValue() public { + MockOrbOracle feed = new MockOrbOracle(-1, 110 * 1e8, 90 * 1e8, 3, "ORB / USD"); + OrbOracleToOracleAdapter adapter = new OrbOracleToOracleAdapter(address(feed), 8, 3); + + vm.expectRevert("bad value"); + adapter.readValue(); + } + + function testRevertsOnZeroFeedAddress() public { + vm.expectRevert("invalid feed"); + new OrbOracleToOracleAdapter(address(0), 8, 3); + } + + function testRevertsOnZeroSampleSize() public { + MockOrbOracle feed = new MockOrbOracle(100 * 1e8, 110 * 1e8, 90 * 1e8, 3, "ORB / USD"); + + vm.expectRevert("invalid sample size"); + new OrbOracleToOracleAdapter(address(feed), 8, 0); + } + + function testScalesZeroDecimalsToWad() public { + MockOrbOracle feed = new MockOrbOracle(1234, 1300, 1200, 3, "ORB / USD"); + OrbOracleToOracleAdapter adapter = new OrbOracleToOracleAdapter(address(feed), 0, 3); + + assertEq(adapter.readValue(), 1234 * 1e18, "0 dec scaling failed"); + } +}