Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

TRUST M-11 #979

Merged
merged 13 commits into from
Oct 19, 2023
34 changes: 21 additions & 13 deletions contracts/plugins/assets/RTokenAsset.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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();
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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 (
Expand All @@ -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(
Expand Down Expand Up @@ -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();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
9 changes: 8 additions & 1 deletion contracts/plugins/mocks/AssetMock.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
})
}

/*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
})
}

/*
Expand Down