diff --git a/contracts/plugins/assets/RTokenAsset.sol b/contracts/plugins/assets/RTokenAsset.sol index f187651e34..f82f2ee185 100644 --- a/contracts/plugins/assets/RTokenAsset.sol +++ b/contracts/plugins/assets/RTokenAsset.sol @@ -19,12 +19,14 @@ contract RTokenAsset is IAsset, VersionedAsset, IRTokenOracle { // Component addresses are not mutable in protocol, so it's safe to cache these IMain public immutable main; - IBasketHandler public immutable basketHandler; IAssetRegistry public immutable assetRegistry; IBackingManager public immutable backingManager; + IBasketHandler public immutable basketHandler; IFurnace public immutable furnace; + IERC20 public immutable rsr; + IStRSR public immutable stRSR; - IERC20Metadata public immutable erc20; + IERC20Metadata public immutable erc20; // The RToken uint8 public immutable erc20Decimals; @@ -39,10 +41,12 @@ contract RTokenAsset is IAsset, VersionedAsset, IRTokenOracle { require(maxTradeVolume_ > 0, "invalid max trade volume"); main = erc20_.main(); - basketHandler = main.basketHandler(); assetRegistry = main.assetRegistry(); backingManager = main.backingManager(); + basketHandler = main.basketHandler(); furnace = main.furnace(); + rsr = main.rsr(); + stRSR = main.stRSR(); erc20 = IERC20Metadata(address(erc20_)); erc20Decimals = erc20_.decimals(); @@ -79,18 +83,15 @@ contract RTokenAsset is IAsset, VersionedAsset, IRTokenOracle { assert(low <= high); // not obviously true } - // solhint-disable no-empty-blocks function refresh() public virtual override { // No need to save lastPrice; can piggyback off the backing collateral's saved prices - if (msg.sender != address(assetRegistry)) assetRegistry.refresh(); furnace.melt(); + if (msg.sender != address(assetRegistry)) assetRegistry.refresh(); cachedOracleData.cachedAtTime = 0; // force oracle refresh } - // solhint-enable no-empty-blocks - /// Should not revert /// @dev See `tryPrice` caveat about possible compounding error in calculating price /// @return {UoA/tok} The lower end of the price estimate @@ -130,10 +131,15 @@ contract RTokenAsset is IAsset, VersionedAsset, IRTokenOracle { // solhint-enable no-empty-blocks + /// Force an update to the cache, including refreshing underlying assets + /// @dev Can revert if RToken is unpriced function forceUpdatePrice() external { _updateCachedPrice(); } + /// @dev Can revert if RToken is unpriced + /// @return rTokenPrice {UoA/tok} The mean price estimate + /// @return updatedAt {s} The timestamp of the cache update function latestPrice() external returns (uint192 rTokenPrice, uint256 updatedAt) { // Situations that require an update, from most common to least common. if ( @@ -145,15 +151,17 @@ contract RTokenAsset is IAsset, VersionedAsset, IRTokenOracle { _updateCachedPrice(); } - return (cachedOracleData.cachedPrice, cachedOracleData.cachedAtTime); + rTokenPrice = cachedOracleData.cachedPrice; + updatedAt = cachedOracleData.cachedAtTime; } // ==== Private ==== // Update Oracle Data function _updateCachedPrice() internal { - (uint192 low, uint192 high) = price(); + assetRegistry.refresh(); // will call furnace.melt() + (uint192 low, uint192 high) = price(); require(low != 0 && high != FIX_MAX, "invalid price"); cachedOracleData = CachedOracleData( @@ -183,12 +191,12 @@ contract RTokenAsset is IAsset, VersionedAsset, IRTokenOracle { TradingContext memory ctx; ctx.basketsHeld = basketsHeld; + ctx.ar = assetRegistry; ctx.bm = backingManager; ctx.bh = basketHandler; - ctx.ar = assetRegistry; - ctx.stRSR = main.stRSR(); - ctx.rsr = main.rsr(); - ctx.rToken = main.rToken(); + ctx.rsr = rsr; + ctx.rToken = IRToken(address(erc20)); + ctx.stRSR = stRSR; ctx.minTradeVolume = backingManager.minTradeVolume(); ctx.maxTradeSlippage = backingManager.maxTradeSlippage(); diff --git a/contracts/plugins/assets/curve/CurveStableRTokenMetapoolCollateral.sol b/contracts/plugins/assets/curve/CurveStableRTokenMetapoolCollateral.sol index 420e002f4a..780a083a8b 100644 --- a/contracts/plugins/assets/curve/CurveStableRTokenMetapoolCollateral.sol +++ b/contracts/plugins/assets/curve/CurveStableRTokenMetapoolCollateral.sol @@ -42,6 +42,11 @@ contract CurveStableRTokenMetapoolCollateral is CurveStableMetapoolCollateral { pairedAssetRegistry = IRToken(address(pairedToken)).main().assetRegistry(); } + function refresh() public override { + pairedAssetRegistry.refresh(); // refresh all registered assets + super.refresh(); // already handles all necessary default checks + } + /// Can revert, used by `_anyDepeggedOutsidePool()` /// Should not return FIX_MAX for low /// @return lowPaired {UoA/pairedTok} The low price estimate of the paired token diff --git a/contracts/plugins/mocks/AssetMock.sol b/contracts/plugins/mocks/AssetMock.sol index c1b495380f..0396a5ea35 100644 --- a/contracts/plugins/mocks/AssetMock.sol +++ b/contracts/plugins/mocks/AssetMock.sol @@ -4,6 +4,8 @@ pragma solidity 0.8.19; import "../assets/Asset.sol"; contract AssetMock is Asset { + bool public stale; + uint192 private lowPrice; uint192 private highPrice; @@ -40,13 +42,18 @@ contract AssetMock is Asset { uint192 ) { + require(!stale, "stale price"); return (lowPrice, highPrice, 0); } /// Should not revert /// Refresh saved prices function refresh() public virtual override { - // pass + stale = false; + } + + function setStale(bool _stale) external { + stale = _stale; } function setPrice(uint192 low, uint192 high) external { diff --git a/test/plugins/individual-collateral/curve/crv/CrvStableRTokenMetapoolTestSuite.test.ts b/test/plugins/individual-collateral/curve/crv/CrvStableRTokenMetapoolTestSuite.test.ts index a97702d5af..fd62e8ee75 100644 --- a/test/plugins/individual-collateral/curve/crv/CrvStableRTokenMetapoolTestSuite.test.ts +++ b/test/plugins/individual-collateral/curve/crv/CrvStableRTokenMetapoolTestSuite.test.ts @@ -241,6 +241,41 @@ const collateralSpecificStatusTests = () => { // refresh() should not revert await collateral.refresh() }) + + it('Regression test -- refreshes inner RTokenAsset on refresh()', async () => { + const [collateral] = await deployCollateral({}) + const initialPrice = await collateral.price() + expect(initialPrice[0]).to.be.gt(0) + expect(initialPrice[1]).to.be.lt(MAX_UINT192) + + // Swap out eUSD's RTokenAsset with a mock one + const AssetMockFactory = await ethers.getContractFactory('AssetMock') + const mockRTokenAsset = await AssetMockFactory.deploy( + bn('1'), // unused + ONE_ADDRESS, // unused + bn('1'), // unused + eUSD, + bn('1'), // unused + bn('1') // unused + ) + const eUSDAssetRegistry = await ethers.getContractAt( + 'IAssetRegistry', + '0x9B85aC04A09c8C813c37de9B3d563C2D3F936162' + ) + await whileImpersonating('0xc8Ee187A5e5c9dC9b42414Ddf861FFc615446a2c', async (signer) => { + await eUSDAssetRegistry.connect(signer).swapRegistered(mockRTokenAsset.address) + }) + + // Set RTokenAsset price to stale + await mockRTokenAsset.setStale(true) + expect(await mockRTokenAsset.stale()).to.be.true + + // Refresh CurveStableRTokenMetapoolCollateral + await collateral.refresh() + + // Stale should be false again + expect(await mockRTokenAsset.stale()).to.be.false + }) } /* diff --git a/test/plugins/individual-collateral/curve/cvx/CvxStableRTokenMetapoolTestSuite.test.ts b/test/plugins/individual-collateral/curve/cvx/CvxStableRTokenMetapoolTestSuite.test.ts index bfb1f30180..ab50ef36a4 100644 --- a/test/plugins/individual-collateral/curve/cvx/CvxStableRTokenMetapoolTestSuite.test.ts +++ b/test/plugins/individual-collateral/curve/cvx/CvxStableRTokenMetapoolTestSuite.test.ts @@ -243,6 +243,41 @@ const collateralSpecificStatusTests = () => { // refresh() should not revert await collateral.refresh() }) + + it('Regression test -- refreshes inner RTokenAsset on refresh()', async () => { + const [collateral] = await deployCollateral({}) + const initialPrice = await collateral.price() + expect(initialPrice[0]).to.be.gt(0) + expect(initialPrice[1]).to.be.lt(MAX_UINT192) + + // Swap out eUSD's RTokenAsset with a mock one + const AssetMockFactory = await ethers.getContractFactory('AssetMock') + const mockRTokenAsset = await AssetMockFactory.deploy( + bn('1'), // unused + ONE_ADDRESS, // unused + bn('1'), // unused + eUSD, + bn('1'), // unused + bn('1') // unused + ) + const eUSDAssetRegistry = await ethers.getContractAt( + 'IAssetRegistry', + '0x9B85aC04A09c8C813c37de9B3d563C2D3F936162' + ) + await whileImpersonating('0xc8Ee187A5e5c9dC9b42414Ddf861FFc615446a2c', async (signer) => { + await eUSDAssetRegistry.connect(signer).swapRegistered(mockRTokenAsset.address) + }) + + // Set RTokenAsset price to stale + await mockRTokenAsset.setStale(true) + expect(await mockRTokenAsset.stale()).to.be.true + + // Refresh CurveStableRTokenMetapoolCollateral + await collateral.refresh() + + // Stale should be false again + expect(await mockRTokenAsset.stale()).to.be.false + }) } /*