Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

- Delay automatic rebalancing after a collateral default while allowing issuance against the backup basket.
- Allow governance-triggered basket changes to rebalance without waiting for `tradingDelay`.
- Prevent unregistering RSR and require RSR assets to preserve BackingManager tradeability.
- Limit BackingManager `minTradeVolume` increases to one tenth of both the conservative RSR stake value and the RSR asset's `maxTradeVolume`.

# 4.2.0

Expand Down
15 changes: 15 additions & 0 deletions contracts/p0/AssetRegistry.sol
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@
/// @custom:governance
function unregister(IAsset asset) external governance {
require(address(asset.erc20()) != address(main.rToken()), "cannot unregister RToken");
require(address(asset.erc20()) != address(main.rsr()), "cannot unregister RSR");
require(_erc20s.contains(address(asset.erc20())), "no asset to unregister");
require(assets[asset.erc20()] == asset, "asset not found");

Expand Down Expand Up @@ -141,7 +142,7 @@
assert(reg.erc20s.length == reg.assets.length);
}

function validateCurrentAssets() external view {}

Check warning on line 145 in contracts/p0/AssetRegistry.sol

View workflow job for this annotation

GitHub Actions / Lint Checks

Code contains empty blocks

/// @return The number of registered ERC20s
function size() external view returns (uint256) {
Expand Down Expand Up @@ -175,6 +176,20 @@
// Refresh to ensure it does not revert, and to save a recent lastPrice
asset.refresh();

if (address(asset.erc20()) == address(main.rsr())) {
(uint192 low, uint192 high) = asset.price();
require(low > 0 && high < FIX_MAX, "RSR asset unpriced");

uint192 requiredVolume = main.backingManager().minTradeVolume() * 10;
require(asset.maxTradeVolume() >= requiredVolume, "RSR maxTradeVolume too low");
if (!_isInitializing()) {
require(
asset.bal(address(main.stRSR())).safeMul(low, FLOOR) >= requiredVolume,
"RSR stake too small"
);
}
}

if (_erc20s.contains(address(asset.erc20())) && assets[asset.erc20()] != asset) {
_erc20s.remove(address(asset.erc20()));
emit AssetUnregistered(asset.erc20(), assets[asset.erc20()]);
Expand Down
14 changes: 14 additions & 0 deletions contracts/p0/BackingManager.sol
Original file line number Diff line number Diff line change
Expand Up @@ -276,4 +276,18 @@ contract BackingManagerP0 is TradingP0, IBackingManager {
emit BackingBufferSet(backingBuffer, val);
backingBuffer = val;
}

function _validateMinTradeVolume(uint192 val) internal view override {
if (_isInitializing()) return;

IAsset rsrAsset = main.assetRegistry().toAsset(main.rsr());
uint192 requiredVolume = val * 10;
require(rsrAsset.maxTradeVolume() >= requiredVolume, "RSR maxTradeVolume too low");

(uint192 low, ) = rsrAsset.price();
require(
rsrAsset.bal(address(main.stRSR())).safeMul(low, FLOOR) >= requiredVolume,
"RSR stake too small"
);
}
}
4 changes: 4 additions & 0 deletions contracts/p0/mixins/Trading.sol
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,11 @@ abstract contract TradingP0 is RewardableP0, ITrading {
/// @custom:governance
function setMinTradeVolume(uint192 val) public governance {
require(val <= MAX_TRADE_VOLUME, "invalid minTradeVolume");
if (val > minTradeVolume) _validateMinTradeVolume(val);
emit MinTradeVolumeSet(minTradeVolume, val);
minTradeVolume = val;
}

// solhint-disable-next-line no-empty-blocks
function _validateMinTradeVolume(uint192) internal view virtual {}
}
17 changes: 17 additions & 0 deletions contracts/p1/AssetRegistry.sol
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,13 @@ import "@openzeppelin/contracts/utils/structs/EnumerableSet.sol";
import "../plugins/assets/RTokenAsset.sol";
import "../interfaces/IAssetRegistry.sol";
import "../interfaces/IMain.sol";
import "../libraries/Fixed.sol";
import "./mixins/Component.sol";

/// The AssetRegistry provides the mapping from ERC20 to Asset, allowing the rest of Main
/// to think in terms of ERC20 tokens and target/ref units.
contract AssetRegistryP1 is ComponentP1, IAssetRegistry {
using FixLib for uint192;
using EnumerableSet for EnumerableSet.AddressSet;

uint256 public constant GAS_FOR_BH_QTY = 100_000; // enough to call bh.quantity
Expand Down Expand Up @@ -132,6 +134,7 @@ contract AssetRegistryP1 is ComponentP1, IAssetRegistry {
IERC20Metadata erc20 = asset.erc20();

require(address(erc20) != address(main.rToken()), "cannot unregister RToken");
require(address(erc20) != address(main.rsr()), "cannot unregister RSR");
require(_erc20s.contains(address(erc20)), "no asset to unregister");
require(assets[erc20] == asset, "asset not found");

Expand Down Expand Up @@ -278,6 +281,20 @@ contract AssetRegistryP1 is ComponentP1, IAssetRegistry {
// Refresh to ensure it does not revert, and to save a recent lastPrice
asset.refresh();

if (address(erc20) == address(main.rsr())) {
(uint192 low, uint192 high) = asset.price();
require(low > 0 && high < FIX_MAX, "RSR asset unpriced");

uint192 requiredVolume = backingManager.minTradeVolume() * 10;
require(asset.maxTradeVolume() >= requiredVolume, "RSR maxTradeVolume too low");
if (!_isInitializing()) {
require(
asset.bal(address(main.stRSR())).safeMul(low, FLOOR) >= requiredVolume,
"RSR stake too small"
);
}
}

if (!main.frozen()) {
backingManager.grantRTokenAllowance(erc20);
}
Expand Down
14 changes: 14 additions & 0 deletions contracts/p1/BackingManager.sol
Original file line number Diff line number Diff line change
Expand Up @@ -347,6 +347,20 @@ contract BackingManagerP1 is TradingP1, IBackingManager {
backingBuffer = val;
}

function _validateMinTradeVolume(uint192 val) internal view override {
if (_isInitializing()) return;

IAsset rsrAsset = assetRegistry.toAsset(rsr);
uint192 requiredVolume = val * 10;
require(rsrAsset.maxTradeVolume() >= requiredVolume, "RSR maxTradeVolume too low");

(uint192 low, ) = rsrAsset.price();
require(
rsrAsset.bal(address(stRSR)).safeMul(low, FLOOR) >= requiredVolume,
"RSR stake too small"
);
}

/// Call after upgrade to >= 3.0.0
function cacheComponents() public {
assetRegistry = main.assetRegistry();
Expand Down
4 changes: 4 additions & 0 deletions contracts/p1/mixins/Trading.sol
Original file line number Diff line number Diff line change
Expand Up @@ -174,10 +174,14 @@ abstract contract TradingP1 is Multicall, ComponentP1, ReentrancyGuardUpgradeabl
function setMinTradeVolume(uint192 val) public {
requireGovernanceOnly();
require(val <= MAX_TRADE_VOLUME, "invalid minTradeVolume");
if (val > minTradeVolume) _validateMinTradeVolume(val);
emit MinTradeVolumeSet(minTradeVolume, val);
minTradeVolume = val;
}

// solhint-disable-next-line no-empty-blocks
function _validateMinTradeVolume(uint192) internal view virtual {}

/**
* @dev This empty reserved space is put in place to allow future versions to add new
* variables without shifting down storage in the inheritance chain.
Expand Down
107 changes: 99 additions & 8 deletions test/Main.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import { expectPrice, expectUnpriced, setOraclePrice } from './utils/oracles'
import { bn, fp } from '../common/numbers'
import {
Asset,
AssetMock,
ATokenFiatCollateral,
BackingManagerP1,
BasketHandlerP1,
Expand Down Expand Up @@ -1072,7 +1073,7 @@ describe(`MainP${IMPLEMENTATION} contract`, () => {
})

it('Should allow to update minTradeVolume if OWNER and perform validations', async () => {
const newValue: BigNumber = fp('0.02')
const newValue: BigNumber = fp('0.005')

// Check existing value
expect(await backingManager.minTradeVolume()).to.equal(config.minTradeVolume)
Expand All @@ -1097,6 +1098,33 @@ describe(`MainP${IMPLEMENTATION} contract`, () => {
).to.be.revertedWith('invalid minTradeVolume')
})

it('Should bound minTradeVolume increases by RSR tradeability', async () => {
const stakeAmt = fp('2000000')
await rsr.mint(addr1.address, stakeAmt)
await rsr.connect(addr1).approve(stRSR.address, stakeAmt)
await stRSR.connect(addr1).stake(stakeAmt)

const maxAllowed = (await rsrAsset.maxTradeVolume()).div(10)
await backingManager.connect(owner).setMinTradeVolume(maxAllowed)
await expect(
backingManager.connect(owner).setMinTradeVolume(maxAllowed.add(1))
).to.be.revertedWith('RSR maxTradeVolume too low')

// RevenueTrader settings are independent
await expect(rsrTrader.connect(owner).setMinTradeVolume(maxAllowed.add(1))).to.not.be.reverted

// Reductions remain available when the stake no longer satisfies the bound
await rsr.burn(stRSR.address, stakeAmt.sub(fp('1')))
await backingManager.connect(owner).setMinTradeVolume(config.minTradeVolume)

const [low] = await rsrAsset.price()
const poolAllowed = low.div(10) // one RSR remains in StRSR
await backingManager.connect(owner).setMinTradeVolume(poolAllowed)
await expect(
backingManager.connect(owner).setMinTradeVolume(poolAllowed.add(1))
).to.be.revertedWith('RSR stake too small')
})

it('Should allow to update backingBuffer if OWNER and perform validations', async () => {
const newValue: BigNumber = fp('0.02')

Expand Down Expand Up @@ -1577,14 +1605,15 @@ describe(`MainP${IMPLEMENTATION} contract`, () => {
await expect(assetRegistry.toAsset(other.address)).to.be.revertedWith('erc20 unregistered')

// Reverts if no registered asset - After unregister
await expect(assetRegistry.connect(owner).unregister(rsrAsset.address))
await expect(assetRegistry.connect(owner).unregister(aaveAsset.address))
.to.emit(assetRegistry, 'AssetUnregistered')
.withArgs(rsr.address, rsrAsset.address)
await expect(assetRegistry.toAsset(rsr.address)).to.be.revertedWith('erc20 unregistered')
.withArgs(aaveToken.address, aaveAsset.address)
await expect(assetRegistry.toAsset(aaveToken.address)).to.be.revertedWith(
'erc20 unregistered'
)

// Returns correctly the asset
expect(await assetRegistry.toAsset(rToken.address)).to.equal(rTokenAsset.address)
expect(await assetRegistry.toAsset(aaveToken.address)).to.equal(aaveAsset.address)
expect(await assetRegistry.toAsset(compToken.address)).to.equal(compAsset.address)
expect(await assetRegistry.toAsset(token0.address)).to.equal(collateral0.address)
expect(await assetRegistry.toAsset(token1.address)).to.equal(collateral1.address)
Expand Down Expand Up @@ -1693,10 +1722,72 @@ describe(`MainP${IMPLEMENTATION} contract`, () => {
).to.be.revertedWith('cannot swap RToken')
})

it('Should not allow to unregister RToken', async () => {
it('Should not allow to unregister RToken or RSR', async () => {
await expect(assetRegistry.connect(owner).unregister(rTokenAsset.address)).to.be.revertedWith(
'cannot unregister RToken'
)
await expect(assetRegistry.connect(owner).unregister(rsrAsset.address)).to.be.revertedWith(
'cannot unregister RSR'
)
})

it('Should preserve RSR tradeability when swapping its asset', async () => {
const AssetFactory: ContractFactory = await ethers.getContractFactory('Asset')
const requiredVolume = config.minTradeVolume.mul(10)

const AssetMockFactory: ContractFactory = await ethers.getContractFactory('AssetMock')
const unpricedAsset: AssetMock = <AssetMock>(
await AssetMockFactory.deploy(
PRICE_TIMEOUT,
await rsrAsset.chainlinkFeed(),
ORACLE_ERROR,
rsr.address,
requiredVolume,
ORACLE_TIMEOUT
)
)
await expect(
assetRegistry.connect(owner).swapRegistered(unpricedAsset.address)
).to.be.revertedWith('RSR asset unpriced')

const lowMaxAsset: Asset = <Asset>(
await AssetFactory.deploy(
PRICE_TIMEOUT,
await rsrAsset.chainlinkFeed(),
ORACLE_ERROR,
rsr.address,
requiredVolume.sub(1),
ORACLE_TIMEOUT
)
)

await expect(
assetRegistry.connect(owner).swapRegistered(lowMaxAsset.address)
).to.be.revertedWith('RSR maxTradeVolume too low')
expect(await assetRegistry.toAsset(rsr.address)).to.equal(rsrAsset.address)

const validAsset: Asset = <Asset>(
await AssetFactory.deploy(
PRICE_TIMEOUT,
await rsrAsset.chainlinkFeed(),
ORACLE_ERROR,
rsr.address,
requiredVolume,
ORACLE_TIMEOUT
)
)
await expect(
assetRegistry.connect(owner).swapRegistered(validAsset.address)
).to.be.revertedWith('RSR stake too small')
expect(await assetRegistry.toAsset(rsr.address)).to.equal(rsrAsset.address)

await rsr.mint(addr1.address, fp('1'))
await rsr.connect(addr1).approve(stRSR.address, fp('1'))
await stRSR.connect(addr1).stake(fp('1'))
await expect(assetRegistry.connect(owner).swapRegistered(validAsset.address))
.to.emit(assetRegistry, 'AssetRegistered')
.withArgs(rsr.address, validAsset.address)
expect(await assetRegistry.toAsset(rsr.address)).to.equal(validAsset.address)
})

context('With quantity reverting', function () {
Expand Down Expand Up @@ -2685,10 +2776,10 @@ describe(`MainP${IMPLEMENTATION} contract`, () => {
expect(await indexBH.status()).to.equal(CollateralStatus.SOUND)
expect(await indexBH.fullyCollateralized()).to.equal(false)

// Unregister everything except token0
// Unregister everything except token0 and the protocol assets
const erc20s = await assetRegistry.erc20s()
for (const erc20 of erc20s) {
if (erc20 != token0.address && erc20 != rToken.address) {
if (erc20 != token0.address && erc20 != rToken.address && erc20 != rsr.address) {
await assetRegistry.connect(owner).unregister(await assetRegistry.toAsset(erc20))
}
}
Expand Down
9 changes: 7 additions & 2 deletions test/RToken.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1991,10 +1991,15 @@ describe(`RTokenP${IMPLEMENTATION} contract`, () => {
})

it('Should not revert when redeeming mostly unregistered collateral #fast', async function () {
// Unregister everything except token0
// Unregister everything except token0 and the protocol assets
const erc20s = await assetRegistry.erc20s()
const rsr = await main.rsr()
for (const erc20 of erc20s) {
if (erc20 != token0.address && erc20 != rToken.address) {
if (
erc20 != token0.address &&
erc20 != rToken.address &&
erc20.toLowerCase() != rsr.toLowerCase()
) {
await assetRegistry.connect(owner).unregister(await assetRegistry.toAsset(erc20))
}
}
Expand Down
2 changes: 2 additions & 0 deletions test/Revenues.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1395,7 +1395,9 @@ describe(`Revenues - P${IMPLEMENTATION}`, () => {

// Perform asset swap
await assetRegistry.connect(owner).swapRegistered(newSellAsset.address)
await rsr.mint(stRSR.address, fp('1'))
await assetRegistry.connect(owner).swapRegistered(newRSRAsset.address)
await rsr.burn(stRSR.address, fp('1'))
await basketHandler.refreshBasket()

// Set rewards manually
Expand Down
2 changes: 2 additions & 0 deletions test/scenario/ComplexBasket.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -175,7 +175,9 @@ describe(`Complex Basket - P${IMPLEMENTATION}`, () => {
ORACLE_TIMEOUT
)
)
await rsr.mint(stRSR.address, fp('1'))
await assetRegistry.connect(owner).swapRegistered(newRSRAsset.address)
await rsr.burn(stRSR.address, fp('1'))
rsrAsset = newRSRAsset

/***** Setup Basket ***********/
Expand Down
2 changes: 2 additions & 0 deletions test/scenario/LargeDecimals.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,9 @@ describe(`Large Decimals Basket - P${IMPLEMENTATION}`, () => {
ORACLE_TIMEOUT
)
)
await rsr.mint(stRSR.address, fp('1'))
await assetRegistry.connect(owner).swapRegistered(newRSRAsset.address)
await rsr.burn(stRSR.address, fp('1'))
rsrAsset = newRSRAsset

// Setup reward asset
Expand Down
Loading