diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index d68a29b..ef3071c 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -71,13 +71,18 @@ jobs: with: version: "nightly-de33b6af53005037b463318d2628b5cfcaf39916" + - name: "Navigate to scripts and install dependencies" + run: | + cd scripts + yarn install + - name: "Show the Foundry config" run: "forge config" - name: "Run the tests" env: RPC_MAINNET: ${{ secrets.RPC_MAINNET }} - run: "forge test --fork-url $RPC_MAINNET" + run: "forge test --fork-url $RPC_MAINNET --ffi" - name: "Add test summary" run: | diff --git a/.gitignore b/.gitignore index 4908036..02942b3 100644 --- a/.gitignore +++ b/.gitignore @@ -20,3 +20,8 @@ lcov.info scripts/node_modules scripts/dist scripts/contract-types + +node_modules + +log.txt +getRedstonePayload.log.txt diff --git a/.gitmodules b/.gitmodules index e80ffd8..124ef1f 100644 --- a/.gitmodules +++ b/.gitmodules @@ -4,3 +4,6 @@ [submodule "lib/openzeppelin-contracts"] path = lib/openzeppelin-contracts url = https://github.com/openzeppelin/openzeppelin-contracts +[submodule "lib/redstone-contracts"] + path = lib/redstone-contracts + url = https://github.com/UMAprotocol/redstone-contracts diff --git a/lib/redstone-contracts b/lib/redstone-contracts new file mode 160000 index 0000000..8bcda0a --- /dev/null +++ b/lib/redstone-contracts @@ -0,0 +1 @@ +Subproject commit 8bcda0a5ba9cc3fd1ad9b6869f498e18ce29f7b2 diff --git a/remappings.txt b/remappings.txt index 551d766..1f54288 100644 --- a/remappings.txt +++ b/remappings.txt @@ -1,3 +1,3 @@ ds-test/=lib/forge-std/lib/ds-test/src/ forge-std/=lib/forge-std/src/ -openzeppelin-contracts/=lib/openzeppelin-contracts/ +redstone-oracle/=lib/redstone-contracts/src/ diff --git a/scripts/package.json b/scripts/package.json index 099f6d0..8b8e457 100644 --- a/scripts/package.json +++ b/scripts/package.json @@ -20,6 +20,8 @@ "typescript": "^5.2.2", "axios": "^1.5.1", "dotenv": "^16.3.1", - "ethers": "^5.7.2" + "ethers": "^5.7.2", + "@redstone-finance/protocol": "^0.5.1", + "@redstone-finance/sdk": "^0.5.1" } -} +} \ No newline at end of file diff --git a/scripts/src/RedstoneHelpers/getRedstonePayload.js b/scripts/src/RedstoneHelpers/getRedstonePayload.js new file mode 100644 index 0000000..bac95db --- /dev/null +++ b/scripts/src/RedstoneHelpers/getRedstonePayload.js @@ -0,0 +1,69 @@ +const { appendFileSync } = require("fs"); +const { RedstonePayload } = require("@redstone-finance/protocol"); +const ethers = require("ethers"); +const sdk = require("@redstone-finance/sdk"); +const args = process.argv.slice(2); + +const exit = (code, message) => { + process.stderr.write(message); + appendFileSync("./getRedstonePayload.log.txt", message); + process.exit(code); +}; + +const parsePrice = (value) => { + const hexString = ethers.utils.hexlify(value); + const bigNumberPrice = ethers.BigNumber.from(hexString); + return bigNumberPrice.toNumber(); +}; + +const pickMedian = (arr) => { + if (arr.length === 0) { + throw new Error("Cannot pick median of empty array"); + } + arr.sort((a, b) => a - b); + const middleIndex = Math.floor(arr.length / 2); + if (arr.length % 2 === 0) { + return (arr[middleIndex - 1] + arr[middleIndex]) / 2; + } else { + return arr[middleIndex]; + } +}; + +const main = async () => { + if (args.length === 0) { + exit(1, "You have to provide a data Feed"); + } + + const dataFeed = args[0]; + + const getLatestSignedPrice = await sdk.requestDataPackages({ + dataServiceId: "redstone-primary-prod", + uniqueSignersCount: 3, + dataFeeds: [dataFeed], + urls: ["https://oracle-gateway-1.a.redstone.finance"], + }); + + const prices = getLatestSignedPrice[dataFeed].map((dataPackage) => + parsePrice(dataPackage.dataPackage.dataPoints[0].value) + ); + + const medianPrice = pickMedian(prices); + + const payload = RedstonePayload.prepare(getLatestSignedPrice[dataFeed], ""); + + const timestampMS = + getLatestSignedPrice[dataFeed][0].dataPackage.timestampMilliseconds; + + const encodedData = ethers.utils.defaultAbiCoder.encode( + ["bytes", "uint256", "uint256"], + ["0x" + payload, timestampMS, medianPrice] + ); + + process.stdout.write(encodedData); + + process.exit(0); +}; + +main().catch((error) => { + exit(4, `An error occurred: ${error.message}`); +}); diff --git a/scripts/yarn.lock b/scripts/yarn.lock index 2dfbc83..75e7e53 100644 --- a/scripts/yarn.lock +++ b/scripts/yarn.lock @@ -344,6 +344,40 @@ "@ethersproject/properties" "^5.7.0" "@ethersproject/strings" "^5.7.0" +"@redstone-finance/oracles-smartweave-contracts@^0.5.1": + version "0.5.1" + resolved "https://registry.yarnpkg.com/@redstone-finance/oracles-smartweave-contracts/-/oracles-smartweave-contracts-0.5.1.tgz#fa68f76fb9258e4f037cde4f03b0ad87e8a8e8a2" + integrity sha512-1FwvgRzuAbQyLpeY9WJqViEp2mB3mNJWO78ogjGgCXGNeXsODoUTBWHRpS0GHpWoW8UKR+MC/t0ZSB+E0XsHCA== + +"@redstone-finance/protocol@^0.5.1": + version "0.5.1" + resolved "https://registry.yarnpkg.com/@redstone-finance/protocol/-/protocol-0.5.1.tgz#6fc47001ee386157a989d95c26dea6f2152b3422" + integrity sha512-MJ8BYuQ34xakfD7SA2OidtrLKDm5J5bZLrWZlsYj8hBs14h/vpFQsdbYJGbiIrN17aIb0c7rST0Q314T30BzJg== + dependencies: + ethers "^5.7.2" + +"@redstone-finance/sdk@^0.5.1": + version "0.5.1" + resolved "https://registry.yarnpkg.com/@redstone-finance/sdk/-/sdk-0.5.1.tgz#3676bf4ffce2081b841367f9c98c42651684a226" + integrity sha512-ElJioHHwJWSRUJqcOV+Iog6b501veZ5/QarfxPMaGGne6ZRKlXGDuj8dDglIZvkXwwSN2j6nCcU5km2RHdLN/Q== + dependencies: + "@redstone-finance/oracles-smartweave-contracts" "^0.5.1" + "@redstone-finance/protocol" "^0.5.1" + "@redstone-finance/utils" "^0.5.1" + axios "^1.6.2" + ethers "^5.7.2" + +"@redstone-finance/utils@^0.5.1": + version "0.5.1" + resolved "https://registry.yarnpkg.com/@redstone-finance/utils/-/utils-0.5.1.tgz#f91ca100ce1a1c870892db40249aebabaa0e411a" + integrity sha512-ui8scibt3mnHC9FjLVxT2k0GrDjlptIvAvNDCRsxm0jL+uttLgMmEp0kZkZ62aWFtSS0ORNDN8cER43yNNWWmQ== + dependencies: + axios "^1.6.2" + consola "^2.15.3" + decimal.js "^10.4.3" + ethers "^5.7.2" + zod "^3.22.4" + "@typechain/ethers-v5@^11.1.2": version "11.1.2" resolved "https://registry.yarnpkg.com/@typechain/ethers-v5/-/ethers-v5-11.1.2.tgz#82510c1744f37a2f906b9e0532ac18c0b74ffe69" @@ -407,6 +441,15 @@ axios@^1.5.1: form-data "^4.0.0" proxy-from-env "^1.1.0" +axios@^1.6.2: + version "1.6.8" + resolved "https://registry.yarnpkg.com/axios/-/axios-1.6.8.tgz#66d294951f5d988a00e87a0ffb955316a619ea66" + integrity sha512-v/ZHtJDU39mDpyBoFVkETcd/uNdxrWRrg3bKpOKzXFA6Bvqopts6ALSMU3y6ijYxbw2B+wPrIv46egTzJXCLGQ== + dependencies: + follow-redirects "^1.15.6" + form-data "^4.0.0" + proxy-from-env "^1.1.0" + balanced-match@^1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" @@ -513,6 +556,11 @@ concat-map@0.0.1: resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg== +consola@^2.15.3: + version "2.15.3" + resolved "https://registry.yarnpkg.com/consola/-/consola-2.15.3.tgz#2e11f98d6a4be71ff72e0bdf07bd23e12cb61550" + integrity sha512-9vAdYbHj6x2fLKC4+oPH0kFzY/orMZyG2Aj+kNylHxKGJ/Ed4dpNyAQYwJOdqO4zdM7XpVHmyejQDcQHrnuXbw== + debug@^4.3.1: version "4.3.4" resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865" @@ -520,6 +568,11 @@ debug@^4.3.1: dependencies: ms "2.1.2" +decimal.js@^10.4.3: + version "10.4.3" + resolved "https://registry.yarnpkg.com/decimal.js/-/decimal.js-10.4.3.tgz#1044092884d245d1b7f65725fa4ad4c6f781cc23" + integrity sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA== + deep-extend@~0.6.0: version "0.6.0" resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.6.0.tgz#c4fa7c95404a17a9c3e8ca7e1537312b736330ac" @@ -601,6 +654,11 @@ follow-redirects@^1.15.0: resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.3.tgz#fe2f3ef2690afce7e82ed0b44db08165b207123a" integrity sha512-1VzOtuEM8pC9SFU1E+8KfTjZyMztRsgEfwQl44z8A25uy13jSzTj6dyK2Df52iV0vgHCfBwLhDWevLn95w5v6Q== +follow-redirects@^1.15.6: + version "1.15.6" + resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.6.tgz#7f815c0cda4249c74ff09e95ef97c23b5fd0399b" + integrity sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA== + form-data@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.0.tgz#93919daeaf361ee529584b9b31664dc12c9fa452" @@ -876,3 +934,8 @@ ws@7.4.6: version "7.4.6" resolved "https://registry.yarnpkg.com/ws/-/ws-7.4.6.tgz#5654ca8ecdeee47c33a9a4bf6d28e2be2980377c" integrity sha512-YmhHDO4MzaDLB+M9ym/mDA5z0naX8j7SIlT8f8z+I0VtzsRbekxEutHSme7NPS2qE8StCYQNUnfWdXta/Yu85A== + +zod@^3.22.4: + version "3.23.8" + resolved "https://registry.yarnpkg.com/zod/-/zod-3.23.8.tgz#e37b957b5d52079769fb8097099b592f0ef4067d" + integrity sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g== diff --git a/test/fork/adapters/RedStoneAsChainlinkSourceAdapter.sol b/test/fork/adapters/RedStoneAsChainlinkSourceAdapter.sol new file mode 100644 index 0000000..d1430eb --- /dev/null +++ b/test/fork/adapters/RedStoneAsChainlinkSourceAdapter.sol @@ -0,0 +1,154 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.17; + +// These tests show that we can treat redstone exactly as chainlink and use it within the Oval ecosystem without the +// need for a new adapter. This assumes the use of Redstone Classic. + +import {CommonTest} from "../../Common.sol"; +import {BaseController} from "../../../src/controllers/BaseController.sol"; + +import {DecimalLib} from "../../../src/adapters/lib/DecimalLib.sol"; +import {ChainlinkSourceAdapter} from "../../../src/adapters/source-adapters/ChainlinkSourceAdapter.sol"; + +import {IAggregatorV3Source} from "../../../src/interfaces/chainlink/IAggregatorV3Source.sol"; +import {MergedPriceFeedAdapterWithRounds} from + "redstone-oracle/on-chain-relayer/contracts/price-feeds/with-rounds/MergedPriceFeedAdapterWithRounds.sol"; + +contract TestedSourceAdapter is ChainlinkSourceAdapter { + constructor(IAggregatorV3Source source) ChainlinkSourceAdapter(source) {} + + function internalLatestData() public view override returns (int256, uint256, uint256) {} + + function internalDataAtRound(uint256 roundId) public view override returns (int256, uint256) {} + + function canUnlock(address caller, uint256 cachedLatestTimestamp) public view virtual override returns (bool) {} + + function lockWindow() public view virtual override returns (uint256) {} + + function maxTraversal() public view virtual override returns (uint256) {} +} + +contract RedstoneAsChainlinkSourceAdapterTest is CommonTest { + uint256 targetBlock = 19889008; + + MergedPriceFeedAdapterWithRounds redstone; + TestedSourceAdapter sourceAdapter; + + function setUp() public { + vm.createSelectFork("mainnet", targetBlock); + redstone = MergedPriceFeedAdapterWithRounds(0xdDb6F90fFb4d3257dd666b69178e5B3c5Bf41136); // Redstone weETH + sourceAdapter = new TestedSourceAdapter(IAggregatorV3Source(address(redstone))); + } + + function testCorrectlyStandardizesOutputs() public { + (, int256 latestRedstoneAnswer,, uint256 latestRedstoneTimestamp,) = redstone.latestRoundData(); + (int256 latestSourceAnswer, uint256 latestSourceTimestamp) = sourceAdapter.getLatestSourceData(); + assertTrue(scaleRedstoneTo18(latestRedstoneAnswer) == latestSourceAnswer); + assertTrue(latestSourceTimestamp == latestRedstoneTimestamp); + } + + function testCanApplyRedstoneUpdateToSource() public { + (uint80 roundId, int256 latestAnswer, uint256 latestTimestamp,,) = redstone.latestRoundData(); + // Values read from the contract at block before applying the update. + assertTrue(roundId == 2009); + assertTrue(latestAnswer == 316882263951); + assertTrue(latestTimestamp == 1715934227); + applyKnownRedstoneUpdate(); + (roundId, latestAnswer, latestTimestamp,,) = redstone.latestRoundData(); + assertTrue(roundId == 2010); + assertTrue(latestAnswer == 313659742144); + assertTrue(latestTimestamp == block.timestamp); + } + + function testCorrectlyLooksBackThroughRounds() public { + // Try fetching the price from some periods in the past and make sure it returns the corespending value for + // given historic lookback. By looking at the contract history on-chain around the blocknumber, we can see + // how many rounds back we expect to look. Looking 1 hour back shows no updates were applied in that interval. + // we should be able to query one hour ago and get the latest round data from both the redstone source and the + // adapter. + uint256 targetTime = block.timestamp - 1 hours; + (uint80 latestRound,,,,) = redstone.latestRoundData(); + + (int256 lookBackPrice, uint256 lookBackTimestamp, uint256 roundId) = + sourceAdapter.tryLatestDataAt(targetTime, 10); + (, int256 answer,, uint256 updatedAt,) = redstone.getRoundData(latestRound); + assertTrue(roundId == latestRound); + assertTrue(updatedAt <= targetTime); + assertTrue(scaleRedstoneTo18(answer) == lookBackPrice); + assertTrue(updatedAt == lookBackTimestamp); + + // Next, try looking back 2 hours. by looking on-chain we can see only one update was applied. Therefore we + // should get the values from latestRound -1 (one update applied relative to the "latest" round). + targetTime = block.timestamp - 2 hours; + (lookBackPrice, lookBackTimestamp,) = sourceAdapter.tryLatestDataAt(targetTime, 10); + (, answer,, updatedAt,) = redstone.getRoundData(latestRound - 1); + assertTrue(updatedAt <= targetTime); + assertTrue(scaleRedstoneTo18(answer) == lookBackPrice); + assertTrue(updatedAt == lookBackTimestamp); + + // Next, try land at 2 rounds ago. Again, by looking on-chain, we can see this is ~2 23 mins before the current + // fork timestamp. We should be able to show the value is the oldest value within this interval. + targetTime = block.timestamp - 2 hours - 23 minutes; + (lookBackPrice, lookBackTimestamp,) = sourceAdapter.tryLatestDataAt(targetTime, 10); + (, answer,, updatedAt,) = redstone.getRoundData(latestRound - 2); + assertTrue(updatedAt <= targetTime); + assertTrue(scaleRedstoneTo18(answer) == lookBackPrice); + assertTrue(updatedAt == lookBackTimestamp); + + // Now, try 3 hours old. On-chain there were 5 updates in this interval. we should be able to show the value is + // the oldest value within this interval. + targetTime = block.timestamp - 3 hours; + (lookBackPrice, lookBackTimestamp,) = sourceAdapter.tryLatestDataAt(targetTime, 10); + (, answer,, updatedAt,) = redstone.getRoundData(latestRound - 5); + assertTrue(updatedAt <= targetTime); + assertTrue(scaleRedstoneTo18(answer) == lookBackPrice); + assertTrue(updatedAt == lookBackTimestamp); + } + + function testCorrectlyBoundsMaxLookBack() public { + // If we limit how far we can lookback the source should correctly return the oldest data it can find, up to + // that limit. From the previous tests we showed that looking back 2 hours 23 hours returns the price from round + // 2. If we try look back longer than this we should get the price from round 2, no matter how far we look back, + // if we bound the maximum lookback to 2 rounds. + uint256 targetTime = block.timestamp - 2 hours - 23 minutes; + (int256 lookBackPrice, uint256 lookBackTimestamp,) = sourceAdapter.tryLatestDataAt(targetTime, 2); + (uint80 latestRound,,,,) = redstone.latestRoundData(); + (, int256 answer,, uint256 updatedAt,) = redstone.getRoundData(latestRound - 2); + assertTrue(scaleRedstoneTo18(answer) == lookBackPrice); + assertTrue(updatedAt == lookBackTimestamp); + + // Now, lookback longer than 2 hours. should get the same value as before. + targetTime = block.timestamp - 3 hours; + (lookBackPrice, lookBackTimestamp,) = sourceAdapter.tryLatestDataAt(targetTime, 2); + assertTrue(scaleRedstoneTo18(answer) == lookBackPrice); + assertTrue(updatedAt == lookBackTimestamp); + targetTime = block.timestamp - 10 hours; + (lookBackPrice, lookBackTimestamp,) = sourceAdapter.tryLatestDataAt(targetTime, 2); + assertTrue(scaleRedstoneTo18(answer) == lookBackPrice); + assertTrue(updatedAt == lookBackTimestamp); + } + + function testNonHistoricalData() public { + uint256 targetTime = block.timestamp - 1 hours; + + (, int256 answer,, uint256 updatedAt,) = redstone.latestRoundData(); + + (int256 lookBackPrice, uint256 lookBackTimestamp,) = sourceAdapter.tryLatestDataAt(targetTime, 0); + assertEq(lookBackPrice / 10 ** 10, answer); + assertEq(lookBackTimestamp, updatedAt); + } + + function applyKnownRedstoneUpdate() internal { + // Update payload taken from: https://etherscan.io/tx/0xbcde8a894337a7e1f29dcad1f78cb0246c1b29305b5c62e43cf0e1801acc11c9 + bytes memory updatePayload = + hex"c14c92040000000000000000000000000000000000000000000000000000018f86086c107765455448000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000490793d7c0018f86086c1000000020000001f6c1ccae51e44aa9e5f57431f59cbd228c190ba072e1eb0af683fef58e02ac0b7f6432b819356ed51210945643f7362871371344395bf48df443663ee525dcef1c7765455448000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000490793d7c0018f86086c100000002000000180353e157cebadd3ba9a25140b95360e0deb02f23274d275301e53680c135c7c0033dcf794366503d9d500d9003842d03abe812bc9535b644e1b5f0cee72845f1c00023137313539343036363233373123302e332e3623646174612d7061636b616765732d77726170706572000029000002ed57011e0000"; + vm.prank(0x517a67D809549093bD3Ef7C6195546B8BDF24C04); // Permissioned Redstone updater. + + (bool success,) = address(redstone).call(updatePayload); + require(success, "Failed to update Redstone data"); + } + + function scaleRedstoneTo18(int256 input) public pure returns (int256) { + return (input * 10 ** 18) / 10 ** 8; + } +} diff --git a/test/unit/CoinbaseSourceAdapter.sol b/test/unit/CoinbaseSourceAdapter.sol index df7cf27..120570e 100644 --- a/test/unit/CoinbaseSourceAdapter.sol +++ b/test/unit/CoinbaseSourceAdapter.sol @@ -33,10 +33,10 @@ contract CoinbaseSourceAdapterTest is CommonTest { string public ticker = "ETH"; uint256 public price = 3000e6; - function pushPrice(string memory ticker, uint256 price, uint256 timestamp) public { + function pushPrice(string memory ticker, uint256 priceToPush, uint256 timestamp) public { string memory kind = "price"; - bytes memory encodedData = abi.encode(kind, timestamp, ticker, price); + bytes memory encodedData = abi.encode(kind, timestamp, ticker, priceToPush); bytes32 hash = keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", keccak256(encodedData))); diff --git a/test/unit/RedStoneOracle.sol b/test/unit/RedStoneOracle.sol new file mode 100644 index 0000000..c3c4d15 --- /dev/null +++ b/test/unit/RedStoneOracle.sol @@ -0,0 +1,82 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.17; + +import {RedstoneConsumerNumericBase} from + "@redstone-finance/evm-connector/contracts/core/RedstoneConsumerNumericBase.sol"; + +import {CommonTest} from "../Common.sol"; + +import {BaseController} from "../../src/controllers/BaseController.sol"; +import {ChainlinkSourceAdapter} from "../../src/adapters/source-adapters/ChainlinkSourceAdapter.sol"; +import {DecimalLib} from "../../src/adapters/lib/DecimalLib.sol"; +import {IAggregatorV3Source} from "../../src/interfaces/chainlink/IAggregatorV3Source.sol"; +import {RedstonePriceFeedWithRounds} from "redstone-oracle/examples/RedstonePriceFeedWithRounds.sol"; + +import {IAggregatorV3Source} from "../../src/interfaces/chainlink/IAggregatorV3Source.sol"; + +import {TestedSourceAdapter} from "../fork/adapters/ChainlinkSourceAdapter.sol"; + + +contract MockRedstonePayload is CommonTest { + function getRedstonePayload(string memory priceFeed) public returns (bytes memory) { + string[] memory args = new string[](4); + args[0] = "node"; + args[1] = "--no-warnings"; + args[2] = "./scripts/src/RedstoneHelpers/getRedstonePayload.js"; + args[3] = priceFeed; + + return vm.ffi(args); + } +} + +contract RedstoneOracleAdapterTest is CommonTest, MockRedstonePayload { + RedstonePriceFeedWithRounds redstoneOracle; + TestedSourceAdapter sourceAdapter; + + function setUp() public { + redstoneOracle = new RedstonePriceFeedWithRounds(bytes32("BTC")); + sourceAdapter = new TestedSourceAdapter(IAggregatorV3Source(address(redstoneOracle))); + } + + function pushPrice() internal returns (uint256, uint256) { + bytes memory data = getRedstonePayload("BTC"); + + (bytes memory redstonePayload, uint256 timestampMilliseconds, uint256 updatePrice) = + abi.decode(data, (bytes, uint256, uint256)); + + uint256 timestampSeconds = timestampMilliseconds / 1000; + + vm.warp(timestampSeconds); + bytes memory encodedFunction = abi.encodeWithSignature("updateDataFeedsValues(uint256)", timestampMilliseconds); + bytes memory encodedFunctionWithRedstonePayload = abi.encodePacked(encodedFunction, redstonePayload); + + address(redstoneOracle).call(encodedFunctionWithRedstonePayload); + + return (updatePrice, timestampSeconds); + } + + function testPushPrice() public { + (uint256 updatePrice, uint256 updateTimestamp) = pushPrice(); + (uint80 roundId, int256 answer, uint256 startedAt, uint256 updatedAt, uint80 answeredInRound) = + redstoneOracle.latestRoundData(); + + assertEq(roundId, 1); + assertEq(uint256(answer), updatePrice); + assertEq(startedAt, updateTimestamp); + assertEq(updatedAt, updateTimestamp); + assertEq(answeredInRound, 1); + } + + function testCorrectlyStandardizesOutputs() public { + (uint256 pushedPrice,) = pushPrice(); + (, int256 latestChainlinkAnswer,, uint256 latestChainlinkTimestamp,) = redstoneOracle.latestRoundData(); + (int256 latestSourceAnswer, uint256 latestSourceTimestamp) = sourceAdapter.getLatestSourceData(); + assertTrue(scaleChainlinkTo18(latestChainlinkAnswer) == latestSourceAnswer); + assertTrue(pushedPrice == uint256(latestChainlinkAnswer)); + assertTrue(latestSourceTimestamp == latestChainlinkTimestamp); + } + + function scaleChainlinkTo18(int256 input) public pure returns (int256) { + return (input * 10 ** 18) / 10 ** 8; + } +}