From 01e3fc6871b8efbe91f646dcaec66e8bcd2736b7 Mon Sep 17 00:00:00 2001 From: 0xtj24 Date: Thu, 14 Aug 2025 16:22:53 +0200 Subject: [PATCH 1/4] woEth feed test --- .../ChainlinkBridgeAssetBase.t.sol | 2 +- test/feedForkTests/WOETH.t.sol | 40 +++++++++++++++++++ 2 files changed, 41 insertions(+), 1 deletion(-) create mode 100644 test/feedForkTests/WOETH.t.sol diff --git a/test/feedForkTests/ChainlinkBridgeAssetBase.t.sol b/test/feedForkTests/ChainlinkBridgeAssetBase.t.sol index fe893cd8..a5e13b4b 100644 --- a/test/feedForkTests/ChainlinkBridgeAssetBase.t.sol +++ b/test/feedForkTests/ChainlinkBridgeAssetBase.t.sol @@ -7,7 +7,7 @@ import "src/feeds/ChainlinkBasePriceFeed.sol"; import {ChainlinkBridgeAssetFeed} from "src/feeds/ChainlinkBridgeAssetFeed.sol"; abstract contract ChainlinkBridgeAssetBase is Test { - ChainlinkBridgeAssetFeed feed; + ChainlinkBridgeAssetFeed internal feed; ChainlinkBasePriceFeed collateralToBridgeAssetFeed; // main coin1 feed ChainlinkBasePriceFeed bridgeAssetToUsdFeed; // main coin2 feed diff --git a/test/feedForkTests/WOETH.t.sol b/test/feedForkTests/WOETH.t.sol new file mode 100644 index 00000000..bd85030c --- /dev/null +++ b/test/feedForkTests/WOETH.t.sol @@ -0,0 +1,40 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.19; + +import "forge-std/Test.sol"; +import {ERC4626Feed, IERC4626} from "src/feeds/ERC4626Feed.sol"; +import {ChainlinkBridgeAssetFeed} from "src/feeds/ChainlinkBridgeAssetFeed.sol"; +import {ChainlinkBridgeAssetBase} from "test/feedForkTests/ChainlinkBridgeAssetBase.t.sol"; +import {ChainlinkBasePriceFeed} from "src/feeds/ChainlinkBasePriceFeed.sol"; + +import "forge-std/console2.sol"; + + +contract WOETHFeed is ChainlinkBridgeAssetBase { + ERC4626Feed vaultFeed; + ChainlinkBasePriceFeed ethWrapper; + ChainlinkBasePriceFeed oEthToEthWrapper; + + address oEthToEth = 0x703118C4CbccCBF2AB31913e0f8075fbbb15f563; + address wOeth = 0xDcEe70654261AF21C44c093C300eD3Bb97b78192; + address ethToUsd = 0x5f4eC3Df9cbd43714FE2740f5E3616155c5b8419; + + function setUp() public { + string memory url = vm.rpcUrl("mainnet"); + vm.createSelectFork(url); + oEthToEthWrapper = new ChainlinkBasePriceFeed(address(this), oEthToEth, address(0), 86400); + vaultFeed = new ERC4626Feed(wOeth, address(oEthToEthWrapper)); + ethWrapper = new ChainlinkBasePriceFeed(address(this),ethToUsd, address(0), 3600); + init(address(vaultFeed), address(ethWrapper), true); + } + + function test_woEth() public { + uint256 woEthToOEth = IERC4626(wOeth).previewRedeem(1e18); + uint256 oEthToEthPrice = uint(oEthToEthWrapper.latestAnswer()); + uint256 ethToUsdPrice = uint(ethWrapper.latestAnswer()); + uint256 woEthToEthPrice = woEthToOEth * oEthToEthPrice / 1e18; + uint256 woEthToUsdPrice = woEthToEthPrice * ethToUsdPrice / 1e18; + assertEq(woEthToUsdPrice, uint(feed.latestAnswer())); + console2.log(uint(feed.latestAnswer())); + } +} \ No newline at end of file From 1091aadae19579321a91c3bf65813e808c2845f4 Mon Sep 17 00:00:00 2001 From: 0xtj24 Date: Mon, 18 Aug 2025 12:20:35 +0200 Subject: [PATCH 2/4] add wrappers --- src/feeds/PriceFeedNoStale.sol | 52 +++++++++++++++++++++++++ src/feeds/PriceFeedNoStaleBasic.sol | 59 +++++++++++++++++++++++++++++ test/feedForkTests/WOETH.t.sol | 28 ++++++++++++-- 3 files changed, 136 insertions(+), 3 deletions(-) create mode 100644 src/feeds/PriceFeedNoStale.sol create mode 100644 src/feeds/PriceFeedNoStaleBasic.sol diff --git a/src/feeds/PriceFeedNoStale.sol b/src/feeds/PriceFeedNoStale.sol new file mode 100644 index 00000000..eade3c4b --- /dev/null +++ b/src/feeds/PriceFeedNoStale.sol @@ -0,0 +1,52 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import "src/interfaces/IChainlinkFeed.sol"; + +contract PriceFeedNoStale { + IChainlinkFeed public immutable feed; + string public description; + + constructor( + address _feed + ) { + feed = IChainlinkFeed(_feed); + require(feed.decimals() == 18, "Wrong Decimals"); + description = feed.description(); + } + + /** + * @notice Retrieves the latest round data for the asset token price feed + * @return roundId Will return 0 + * @return usdPrice The latest asset price in USD with 18 decimals + * @return startedAt Will return 0 + * @return updatedAt Will return block.timestamp + * @return answeredInRound Will return 0 + */ + function latestRoundData() + public + view + returns ( + uint80 roundId, + int256 usdPrice, + uint256 startedAt, + uint256 updatedAt, + uint80 answeredInRound + ) + { + return (0, feed.latestAnswer(), 0, block.timestamp, 0 ); + } + + /** + * @notice Returns the latest price only + * @dev Unlike chainlink oracles, the latestAnswer will always be the same as in the latestRoundData + * @return int256 Returns the last finalized price of the chainlink oracle + */ + function latestAnswer() external view returns (int256) { + return feed.latestAnswer(); + } + + function decimals() external pure returns (uint8) { + return 18; + } +} diff --git a/src/feeds/PriceFeedNoStaleBasic.sol b/src/feeds/PriceFeedNoStaleBasic.sol new file mode 100644 index 00000000..6129c4f9 --- /dev/null +++ b/src/feeds/PriceFeedNoStaleBasic.sol @@ -0,0 +1,59 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import "src/interfaces/IChainlinkFeed.sol"; + +contract PriceFeedNoStaleBasic { + IChainlinkFeed public immutable vaultFeed; + IChainlinkFeed public immutable assetToUsd; + string public description; + + constructor( + address _vaultFeed, + address _assetToUsd + ) { + vaultFeed = IChainlinkFeed(_vaultFeed); + assetToUsd = IChainlinkFeed(_assetToUsd); + require(vaultFeed.decimals() == 18 && assetToUsd.decimals() == 18); + description = string(abi.encodePacked(vaultFeed.description(), " * (", assetToUsd.description(),")")); + } + + /** + * @notice Retrieves the latest round data for the asset token price feed + * @return roundId Will return 0 + * @return usdPrice The latest asset price in USD with 18 decimals + * @return startedAt Will return 0 + * @return updatedAt Will return block.timestamp + * @return answeredInRound Will return 0 + */ + function latestRoundData() + public + view + returns ( + uint80 roundId, + int256 usdPrice, + uint256 startedAt, + uint256 updatedAt, + uint80 answeredInRound + ) + { + uint256 vaultPrice = uint(vaultFeed.latestAnswer()); + uint256 assetToUsdPrice = uint(assetToUsd.latestAnswer()); + usdPrice = int(vaultPrice * assetToUsdPrice / 1e18); + return (0, usdPrice, 0, block.timestamp, 0 ); + } + + /** + * @notice Returns the latest price only + * @dev Unlike chainlink oracles, the latestAnswer will always be the same as in the latestRoundData + * @return int256 Returns the last finalized price of the chainlink oracle + */ + function latestAnswer() external view returns (int256) { + (, int price, , ,) = latestRoundData(); + return price; + } + + function decimals() external pure returns (uint8) { + return 18; + } +} diff --git a/test/feedForkTests/WOETH.t.sol b/test/feedForkTests/WOETH.t.sol index bd85030c..f65787ff 100644 --- a/test/feedForkTests/WOETH.t.sol +++ b/test/feedForkTests/WOETH.t.sol @@ -6,15 +6,18 @@ import {ERC4626Feed, IERC4626} from "src/feeds/ERC4626Feed.sol"; import {ChainlinkBridgeAssetFeed} from "src/feeds/ChainlinkBridgeAssetFeed.sol"; import {ChainlinkBridgeAssetBase} from "test/feedForkTests/ChainlinkBridgeAssetBase.t.sol"; import {ChainlinkBasePriceFeed} from "src/feeds/ChainlinkBasePriceFeed.sol"; - +import {PriceFeedNoStale} from "src/feeds/PriceFeedNoStale.sol"; +import {PriceFeedNoStaleBasic} from "src/feeds/PriceFeedNoStaleBasic.sol"; import "forge-std/console2.sol"; -contract WOETHFeed is ChainlinkBridgeAssetBase { +contract WOETHFeedTest is ChainlinkBridgeAssetBase { ERC4626Feed vaultFeed; ChainlinkBasePriceFeed ethWrapper; ChainlinkBasePriceFeed oEthToEthWrapper; - + PriceFeedNoStale feedNoStale; + PriceFeedNoStaleBasic feedNoStaleBasic; + address oEthToEth = 0x703118C4CbccCBF2AB31913e0f8075fbbb15f563; address wOeth = 0xDcEe70654261AF21C44c093C300eD3Bb97b78192; address ethToUsd = 0x5f4eC3Df9cbd43714FE2740f5E3616155c5b8419; @@ -26,6 +29,9 @@ contract WOETHFeed is ChainlinkBridgeAssetBase { vaultFeed = new ERC4626Feed(wOeth, address(oEthToEthWrapper)); ethWrapper = new ChainlinkBasePriceFeed(address(this),ethToUsd, address(0), 3600); init(address(vaultFeed), address(ethWrapper), true); + feedNoStale = new PriceFeedNoStale(address(feed)); + feedNoStaleBasic = new PriceFeedNoStaleBasic(address(vaultFeed),address(ethWrapper)); + } function test_woEth() public { @@ -37,4 +43,20 @@ contract WOETHFeed is ChainlinkBridgeAssetBase { assertEq(woEthToUsdPrice, uint(feed.latestAnswer())); console2.log(uint(feed.latestAnswer())); } + + function test_feedNoStale() public { + assertEq(feed.latestAnswer(), feedNoStale.latestAnswer()); + (,int price, , uint updateAt,) = feedNoStale.latestRoundData(); + assertEq(updateAt, block.timestamp); + assertEq(price, feed.latestAnswer()); + console2.log(feedNoStaleBasic.description()); + } + + function test_feedNoStaleBasic() public { + assertEq(feed.latestAnswer(), feedNoStaleBasic.latestAnswer()); + (,int price, , uint updateAt,) = feedNoStaleBasic.latestRoundData(); + assertEq(updateAt, block.timestamp); + assertEq(price, feed.latestAnswer()); + console2.log(feedNoStaleBasic.description()); + } } \ No newline at end of file From 246c1773bc5881be928821d26d64b644c0a20d9e Mon Sep 17 00:00:00 2001 From: 0xtj24 Date: Mon, 18 Aug 2025 18:29:28 +0200 Subject: [PATCH 3/4] rm PriceFeedNoStaleBasic --- src/feeds/PriceFeedNoStaleBasic.sol | 59 ----------------------------- test/feedForkTests/WOETH.t.sol | 14 +------ 2 files changed, 1 insertion(+), 72 deletions(-) delete mode 100644 src/feeds/PriceFeedNoStaleBasic.sol diff --git a/src/feeds/PriceFeedNoStaleBasic.sol b/src/feeds/PriceFeedNoStaleBasic.sol deleted file mode 100644 index 6129c4f9..00000000 --- a/src/feeds/PriceFeedNoStaleBasic.sol +++ /dev/null @@ -1,59 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.20; - -import "src/interfaces/IChainlinkFeed.sol"; - -contract PriceFeedNoStaleBasic { - IChainlinkFeed public immutable vaultFeed; - IChainlinkFeed public immutable assetToUsd; - string public description; - - constructor( - address _vaultFeed, - address _assetToUsd - ) { - vaultFeed = IChainlinkFeed(_vaultFeed); - assetToUsd = IChainlinkFeed(_assetToUsd); - require(vaultFeed.decimals() == 18 && assetToUsd.decimals() == 18); - description = string(abi.encodePacked(vaultFeed.description(), " * (", assetToUsd.description(),")")); - } - - /** - * @notice Retrieves the latest round data for the asset token price feed - * @return roundId Will return 0 - * @return usdPrice The latest asset price in USD with 18 decimals - * @return startedAt Will return 0 - * @return updatedAt Will return block.timestamp - * @return answeredInRound Will return 0 - */ - function latestRoundData() - public - view - returns ( - uint80 roundId, - int256 usdPrice, - uint256 startedAt, - uint256 updatedAt, - uint80 answeredInRound - ) - { - uint256 vaultPrice = uint(vaultFeed.latestAnswer()); - uint256 assetToUsdPrice = uint(assetToUsd.latestAnswer()); - usdPrice = int(vaultPrice * assetToUsdPrice / 1e18); - return (0, usdPrice, 0, block.timestamp, 0 ); - } - - /** - * @notice Returns the latest price only - * @dev Unlike chainlink oracles, the latestAnswer will always be the same as in the latestRoundData - * @return int256 Returns the last finalized price of the chainlink oracle - */ - function latestAnswer() external view returns (int256) { - (, int price, , ,) = latestRoundData(); - return price; - } - - function decimals() external pure returns (uint8) { - return 18; - } -} diff --git a/test/feedForkTests/WOETH.t.sol b/test/feedForkTests/WOETH.t.sol index f65787ff..cbaae4a8 100644 --- a/test/feedForkTests/WOETH.t.sol +++ b/test/feedForkTests/WOETH.t.sol @@ -7,7 +7,6 @@ import {ChainlinkBridgeAssetFeed} from "src/feeds/ChainlinkBridgeAssetFeed.sol"; import {ChainlinkBridgeAssetBase} from "test/feedForkTests/ChainlinkBridgeAssetBase.t.sol"; import {ChainlinkBasePriceFeed} from "src/feeds/ChainlinkBasePriceFeed.sol"; import {PriceFeedNoStale} from "src/feeds/PriceFeedNoStale.sol"; -import {PriceFeedNoStaleBasic} from "src/feeds/PriceFeedNoStaleBasic.sol"; import "forge-std/console2.sol"; @@ -16,7 +15,6 @@ contract WOETHFeedTest is ChainlinkBridgeAssetBase { ChainlinkBasePriceFeed ethWrapper; ChainlinkBasePriceFeed oEthToEthWrapper; PriceFeedNoStale feedNoStale; - PriceFeedNoStaleBasic feedNoStaleBasic; address oEthToEth = 0x703118C4CbccCBF2AB31913e0f8075fbbb15f563; address wOeth = 0xDcEe70654261AF21C44c093C300eD3Bb97b78192; @@ -30,8 +28,6 @@ contract WOETHFeedTest is ChainlinkBridgeAssetBase { ethWrapper = new ChainlinkBasePriceFeed(address(this),ethToUsd, address(0), 3600); init(address(vaultFeed), address(ethWrapper), true); feedNoStale = new PriceFeedNoStale(address(feed)); - feedNoStaleBasic = new PriceFeedNoStaleBasic(address(vaultFeed),address(ethWrapper)); - } function test_woEth() public { @@ -49,14 +45,6 @@ contract WOETHFeedTest is ChainlinkBridgeAssetBase { (,int price, , uint updateAt,) = feedNoStale.latestRoundData(); assertEq(updateAt, block.timestamp); assertEq(price, feed.latestAnswer()); - console2.log(feedNoStaleBasic.description()); - } - - function test_feedNoStaleBasic() public { - assertEq(feed.latestAnswer(), feedNoStaleBasic.latestAnswer()); - (,int price, , uint updateAt,) = feedNoStaleBasic.latestRoundData(); - assertEq(updateAt, block.timestamp); - assertEq(price, feed.latestAnswer()); - console2.log(feedNoStaleBasic.description()); + console2.log(feedNoStale.description()); } } \ No newline at end of file From 07e1936986f9fe5bdcd21de006f2407d4a636cad Mon Sep 17 00:00:00 2001 From: 0xtj24 Date: Fri, 29 Aug 2025 09:08:09 +0200 Subject: [PATCH 4/4] add sINV feed test --- test/feedForkTests/SINV.t.sol | 41 +++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 test/feedForkTests/SINV.t.sol diff --git a/test/feedForkTests/SINV.t.sol b/test/feedForkTests/SINV.t.sol new file mode 100644 index 00000000..6da24e01 --- /dev/null +++ b/test/feedForkTests/SINV.t.sol @@ -0,0 +1,41 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.19; + +import "forge-std/Test.sol"; +import {ERC4626Feed, IERC4626} from "src/feeds/ERC4626Feed.sol"; +import {ChainlinkBasePriceFeed} from "src/feeds/ChainlinkBasePriceFeed.sol"; +import {PriceFeedNoStale} from "src/feeds/PriceFeedNoStale.sol"; +import "forge-std/console2.sol"; + + +contract SINVFeedTest is Test { + ERC4626Feed vaultFeed; + PriceFeedNoStale priceFeed; + + IERC4626 sInv = IERC4626(0x08d23468A467d2bb86FaE0e32F247A26C7E2e994); + ChainlinkBasePriceFeed invToUsd = ChainlinkBasePriceFeed(0x54F1E4EB93c5b5F4C12776c96e08a49A9928FE84); + + function setUp() public { + string memory url = vm.rpcUrl("mainnet"); + vm.createSelectFork(url); + vaultFeed = new ERC4626Feed(address(sInv), address(invToUsd)); + priceFeed = new PriceFeedNoStale(address(vaultFeed)); + } + + function test_sINV_Feed() public { + uint256 exchangeRate = sInv.previewRedeem(1e18); + uint256 invPrice = uint(invToUsd.latestAnswer()); + uint256 sInvToUsd = exchangeRate * invPrice / 1e18; + assertEq(sInvToUsd, uint(vaultFeed.latestAnswer())); + assertEq(sInvToUsd, uint(priceFeed.latestAnswer())); + console2.log(uint(vaultFeed.latestAnswer())); + } + + function test_feedNoStale() public { + assertEq(vaultFeed.latestAnswer(), priceFeed.latestAnswer()); + (,int price, , uint updateAt,) = priceFeed.latestRoundData(); + assertEq(updateAt, block.timestamp); + assertEq(price, vaultFeed.latestAnswer()); + console2.log(priceFeed.description()); + } +} \ No newline at end of file