Skip to content

Commit

Permalink
Merge branch '2.0.0-candidate-4' into hardhat-toolbox-migration
Browse files Browse the repository at this point in the history
  • Loading branch information
pmckelvy1 committed Feb 22, 2023
2 parents b19f845 + 1867a4a commit ed6b8fe
Show file tree
Hide file tree
Showing 72 changed files with 1,172 additions and 635 deletions.
10 changes: 6 additions & 4 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,10 +45,9 @@ Candidate release for the "all clear" milestone. There wasn't any real usage of
- Bump solidity version to 0.8.17
- Support multiple beneficiaries via the [`FacadeWrite`](contracts/facade/FacadeWrite.sol)
- Add `RToken.issueTo(address recipient, uint256 amount, ..)` and `RToken.redeemTo(address recipient, uint256 amount, ..)` to support issuance/redemption to a different address than `msg.sender`
- Add `RToken.issue*(.., bool revertOnPartialRedemption)` and `RToken.redeem*(.., bool revertOnPartialRedemption)` to enable msg sender to control whether they will accept partial redemptions or not
- Add `RToken.redeem*(.., uint256 basketNonce)` to enable msg sender to control expectations around partial redemptions
- Add `RToken.issuanceAvailable()` + `RToken.redemptionAvailable()`
- Add `FacadeRead.primeBasket()` + `FacadeRead.backupConfig()` views
- Remove `IBasketHandler.nonce()` from interface, though it remains on `BasketHandler` contracts
- Many external libs moved to internal
- Switch from price point estimates to price ranges; all prices now have a `low` and `high`. Impacted interface functions:
- `IAsset.price()`
Expand All @@ -65,7 +64,7 @@ Candidate release for the "all clear" milestone. There wasn't any real usage of
- Add `.div(1 - maxTradeSlippage)` to calculation of `shortfallSlippage` in [RecollateralizationLib.sol:L188](contracts/p1/mixins/RecollateralizationLib.sol).
- FacadeRead:
- remove `.pendingIssuances()` + `.endIdForVest()`
- refactor calculations in `basketBreakdown()`
- refactor calculations in `basketBreakdown()`
- Bugfix: Fix claim rewards from traders in `FacadeAct`
- Bugfix: Do not handout RSR rewards when no one is staked
- Bugfix: Support small redemptions even when the RToken supply is massive
Expand All @@ -89,7 +88,6 @@ Candidate release for the "all clear" milestone. There wasn't any real usage of
- ++`RToken.IssuanceThrottleSet`
- ++`RToken.RedemptionThrottleSet`
- Allow redemption while DISABLED
- Disallow staking while FROZEN
- Allow `grantRTokenAllowances()` while paused
- Add `RToken.monetizeDonations()` escape hatch for accidentally donated tokens
- Collateral default threshold: 5% -> 1% (+ include oracleError)
Expand All @@ -101,3 +99,7 @@ Candidate release for the "all clear" milestone. There wasn't any real usage of
- Accumulate melting on `Furnace.setRatio()`
- Payout RSR rewards on `StRSR.setRatio()`
- Distinguish oracle timeouts when dealing with multiple oracles in one plugin
- Add safety during asset degregistration to ensure it is always possible to unregister an infinite-looping asset
- Fix `StRSR`/`RToken` EIP712 typehash to use release version instead of "1"
- Add `FacadeRead.redeem(IRToken rToken, uint256 amount, uint48 basketNonce)` to return the expected redemption quantities on the basketNonce, or revert
- Integrate with OZ 4.7.3 Governance (changes to `quorum()`/t`proposalThreshold()`)
51 changes: 48 additions & 3 deletions contracts/facade/FacadeRead.sol
Original file line number Diff line number Diff line change
Expand Up @@ -32,18 +32,18 @@ contract FacadeRead is IFacadeRead {
main.poke();
// {BU}

uint192 held = main.basketHandler().basketsHeldBy(account);
BasketRange memory basketsHeld = main.basketHandler().basketsHeldBy(account);
uint192 needed = rToken.basketsNeeded();

int8 decimals = int8(rToken.decimals());

// return {qRTok} = {BU} * {(1 RToken) qRTok/BU)}
if (needed.eq(FIX_ZERO)) return held.shiftl_toUint(decimals);
if (needed.eq(FIX_ZERO)) return basketsHeld.bottom.shiftl_toUint(decimals);

uint192 totalSupply = shiftl_toFix(rToken.totalSupply(), -decimals); // {rTok}

// {qRTok} = {BU} * {rTok} / {BU} * {qRTok/rTok}
return held.mulDiv(totalSupply, needed).shiftl_toUint(decimals);
return basketsHeld.bottom.mulDiv(totalSupply, needed).shiftl_toUint(decimals);
}

/// @custom:static-call
Expand All @@ -64,6 +64,51 @@ contract FacadeRead is IFacadeRead {
(tokens, deposits) = bh.quote(baskets, CEIL);
}

/// @return tokens The erc20s returned for the redemption
/// @return withdrawals The balances necessary to issue `amount` RToken
/// @return isProrata True if the redemption is prorata and not full
/// @custom:static-call
function redeem(
IRToken rToken,
uint256 amount,
uint48 basketNonce
)
external
returns (
address[] memory tokens,
uint256[] memory withdrawals,
bool isProrata
)
{
IMain main = rToken.main();
main.poke();
IRToken rTok = rToken;
IBasketHandler bh = main.basketHandler();
uint256 supply = rTok.totalSupply();
require(bh.nonce() == basketNonce, "non-current basket nonce");

// D18{BU} = D18{BU} * {qRTok} / {qRTok}
uint192 basketsRedeemed = rTok.basketsNeeded().muluDivu(amount, supply);

(tokens, withdrawals) = bh.quote(basketsRedeemed, FLOOR);

// Bound each withdrawal by the prorata share, in case we're currently under-collateralized
address backingManager = address(main.backingManager());
for (uint256 i = 0; i < tokens.length; ++i) {
// {qTok} = {qTok} * {qRTok} / {qRTok}
uint256 prorata = mulDiv256(
IERC20Upgradeable(tokens[i]).balanceOf(backingManager),
amount,
supply
); // FLOOR

if (prorata < withdrawals[i]) {
withdrawals[i] = prorata;
isProrata = true;
}
}
}

/// @return erc20s The ERC20 addresses in the current basket
/// @return uoaShares {1} The proportion of the basket associated with each ERC20
/// @return targets The bytes32 representations of the target unit associated with each ERC20
Expand Down
7 changes: 7 additions & 0 deletions contracts/facade/FacadeTest.sol
Original file line number Diff line number Diff line change
Expand Up @@ -89,4 +89,11 @@ contract FacadeTest is IFacadeTest {
total = total.plus(asset.bal(backingManager).mul(midPrice));
}
}

/// @param account The account to count baskets for
/// @return {BU} The number of whole basket units held
function wholeBasketsHeldBy(IRToken rToken, address account) external view returns (uint192) {
BasketRange memory range = rToken.main().basketHandler().basketsHeldBy(account);
return range.bottom;
}
}
4 changes: 0 additions & 4 deletions contracts/facade/FacadeWrite.sol
Original file line number Diff line number Diff line change
Expand Up @@ -57,13 +57,11 @@ contract FacadeWrite is IFacadeWrite {
// Get Main
IMain main = rToken.main();
IAssetRegistry assetRegistry = main.assetRegistry();
IBackingManager backingManager = main.backingManager();
IBasketHandler basketHandler = main.basketHandler();

// Register assets
for (uint256 i = 0; i < setup.assets.length; ++i) {
require(assetRegistry.register(setup.assets[i]), "duplicate asset");
backingManager.grantRTokenAllowance(setup.assets[i].erc20());
}

// Setup basket
Expand All @@ -75,7 +73,6 @@ contract FacadeWrite is IFacadeWrite {
require(assetRegistry.register(setup.primaryBasket[i]), "duplicate collateral");
IERC20 erc20 = setup.primaryBasket[i].erc20();
basketERC20s[i] = erc20;
backingManager.grantRTokenAllowance(erc20);
}

// Set basket
Expand All @@ -95,7 +92,6 @@ contract FacadeWrite is IFacadeWrite {
assetRegistry.register(backupColl); // do not require the asset is new
IERC20 erc20 = backupColl.erc20();
backupERC20s[j] = erc20;
backingManager.grantRTokenAllowance(erc20);
}

basketHandler.setBackupConfig(
Expand Down
13 changes: 11 additions & 2 deletions contracts/interfaces/IBasketHandler.sol
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,11 @@ import "../libraries/Fixed.sol";
import "./IAsset.sol";
import "./IComponent.sol";

struct BasketRange {
uint192 bottom; // {BU}
uint192 top; // {BU}
}

/**
* @title IBasketHandler
* @notice The BasketHandler aims to maintain a reference basket of constant target unit amounts.
Expand Down Expand Up @@ -94,8 +99,9 @@ interface IBasketHandler is IComponent {
view
returns (address[] memory erc20s, uint256[] memory quantities);

/// @return baskets {BU} The quantity of complete baskets at an address. A balance for BUs
function basketsHeldBy(address account) external view returns (uint192 baskets);
/// @return top {BU} The number of partial basket units: e.g max(coll.map((c) => c.balAsBUs())
/// bottom {BU} The number of whole basket units held by the account
function basketsHeldBy(address account) external view returns (BasketRange memory);

/// Should not revert
/// @return low {UoA/BU} The lower end of the price estimate
Expand All @@ -110,4 +116,7 @@ interface IBasketHandler is IComponent {

/// @return timestamp The timestamp at which the basket was last set
function timestamp() external view returns (uint48);

/// @return The current basket nonce, regardless of status
function nonce() external view returns (uint48);
}
16 changes: 16 additions & 0 deletions contracts/interfaces/IFacadeRead.sol
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,22 @@ interface IFacadeRead {
external
returns (address[] memory tokens, uint256[] memory deposits);

/// @return tokens The erc20s returned for the redemption
/// @return withdrawals The balances necessary to issue `amount` RToken
/// @return isProrata True if the redemption is prorata and not full
/// @custom:static-call
function redeem(
IRToken rToken,
uint256 amount,
uint48 basketNonce
)
external
returns (
address[] memory tokens,
uint256[] memory withdrawals,
bool isProrata
);

/// @return erc20s The ERC20 addresses in the current basket
/// @return uoaShares The proportion of the basket associated with each ERC20
/// @return targets The bytes32 representations of the target unit associated with each ERC20
Expand Down
4 changes: 4 additions & 0 deletions contracts/interfaces/IFacadeTest.sol
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,8 @@ interface IFacadeTest {
/// @return total {UoA} An estimate of the total value of all assets held at BackingManager
/// @custom:static-call
function totalAssetValue(IRToken rToken) external returns (uint192 total);

/// @param account The account to count baskets for
/// @return {BU} The number of basket units helds
function wholeBasketsHeldBy(IRToken rToken, address account) external view returns (uint192);
}
5 changes: 3 additions & 2 deletions contracts/interfaces/IMain.sol
Original file line number Diff line number Diff line change
Expand Up @@ -67,8 +67,9 @@ interface IAuth is IAccessControlUpgradeable {
event PausedSet(bool indexed oldVal, bool indexed newVal);

/**
* Paused: Disable everything except for OWNER actions and RToken.redeem/cancel
* Frozen: Disable everything except for OWNER actions
* Paused: Disable everything except for OWNER actions, RToken.redeem, StRSR.stake,
* and StRSR.payoutRewards
* Frozen: Disable everything except for OWNER actions + StRSR.stake (for governance)
*/

function pausedOrFrozen() external view returns (bool);
Expand Down
8 changes: 4 additions & 4 deletions contracts/interfaces/IRToken.sol
Original file line number Diff line number Diff line change
Expand Up @@ -80,19 +80,19 @@ interface IRToken is IComponent, IERC20MetadataUpgradeable, IERC20PermitUpgradea

/// Redeem RToken for basket collateral
/// @param amount {qRTok} The quantity {qRToken} of RToken to redeem
/// @param revertOnPartialRedemption If true, will revert on partial redemption
/// @param basketNonce The nonce of the basket the redemption should be from; else reverts
/// @custom:interaction
function redeem(uint256 amount, bool revertOnPartialRedemption) external;
function redeem(uint256 amount, uint48 basketNonce) external;

/// Redeem RToken for basket collateral to a particular recipient
/// @param recipient The address to receive the backing collateral tokens
/// @param amount {qRTok} The quantity {qRToken} of RToken to redeem
/// @param revertOnPartialRedemption If true, will revert on partial redemption
/// @param basketNonce The nonce of the basket the redemption should be from; else reverts
/// @custom:interaction
function redeemTo(
address recipient,
uint256 amount,
bool revertOnPartialRedemption
uint48 basketNonce
) external;

/// Mints a quantity of RToken to the `recipient`, callable only by the BackingManager
Expand Down
1 change: 0 additions & 1 deletion contracts/interfaces/IStRSR.sol
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,6 @@ interface IStRSR is IERC20MetadataUpgradeable, IERC20PermitUpgradeable, ICompone
event AllUnstakingReset(uint256 indexed newEra);

event UnstakingDelaySet(uint48 indexed oldVal, uint48 indexed newVal);
event RewardPeriodSet(uint48 indexed oldVal, uint48 indexed newVal);
event RewardRatioSet(uint192 indexed oldVal, uint192 indexed newVal);

// Initialization
Expand Down
2 changes: 1 addition & 1 deletion contracts/mixins/Auth.sol
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ abstract contract Auth is AccessControlUpgradeable, IAuth {
/**
* System-wide states (does not impact ERC20 functions)
* - Frozen: only allow OWNER actions and staking
* - Paused: only allow OWNER actions, redemption, issuance cancellation, and staking
* - Paused: only allow OWNER actions, redemption, staking, and rewards payout
*
* Typically freezing thaws on its own in a predetemined number of blocks.
* However, OWNER can also freeze forever.
Expand Down
5 changes: 4 additions & 1 deletion contracts/mixins/Versioned.sol
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,15 @@ pragma solidity 0.8.17;

import "../interfaces/IVersioned.sol";

// This value should be updated on each release
string constant VERSION = "2.0.0";

/**
* @title Versioned
* @notice A mix-in to track semantic versioning uniformly across contracts.
*/
abstract contract Versioned is IVersioned {
function version() public pure virtual override returns (string memory) {
return "2.0.0";
return VERSION;
}
}
12 changes: 10 additions & 2 deletions contracts/p0/AssetRegistry.sol
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ contract AssetRegistryP0 is ComponentP0, IAssetRegistry {
using FixLib for uint192;
using EnumerableSet for EnumerableSet.AddressSet;

uint256 public constant GAS_TO_RESERVE = 900000; // just enough to disable basket on n=128

// Registered ERC20s
EnumerableSet.AddressSet private _erc20s;

Expand Down Expand Up @@ -50,7 +52,7 @@ contract AssetRegistryP0 is ComponentP0, IAssetRegistry {
assert(assets[asset.erc20()] != IAsset(address(0)));

IBasketHandler basketHandler = main.basketHandler();
try basketHandler.quantity(asset.erc20()) returns (uint192 quantity) {
try basketHandler.quantity{ gas: _reserveGas() }(asset.erc20()) returns (uint192 quantity) {
if (quantity.gt(0)) basketHandler.disableBasket(); // not an interaction
} catch {
basketHandler.disableBasket();
Expand All @@ -66,7 +68,7 @@ contract AssetRegistryP0 is ComponentP0, IAssetRegistry {
require(assets[asset.erc20()] == asset, "asset not found");

IBasketHandler basketHandler = main.basketHandler();
try basketHandler.quantity(asset.erc20()) returns (uint192 quantity) {
try basketHandler.quantity{ gas: _reserveGas() }(asset.erc20()) returns (uint192 quantity) {
if (quantity.gt(0)) basketHandler.disableBasket(); // not an interaction
} catch {
basketHandler.disableBasket();
Expand Down Expand Up @@ -149,4 +151,10 @@ contract AssetRegistryP0 is ComponentP0, IAssetRegistry {
}
emit AssetRegistered(asset.erc20(), asset);
}

function _reserveGas() private view returns (uint256) {
uint256 gas = gasleft();
require(gas > GAS_TO_RESERVE, "not enough gas to unregister safely");
return gas - GAS_TO_RESERVE;
}
}
23 changes: 12 additions & 11 deletions contracts/p0/BackingManager.sol
Original file line number Diff line number Diff line change
Expand Up @@ -78,8 +78,10 @@ contract BackingManagerP0 is TradingP0, IBackingManager {
uint48 basketTimestamp = main.basketHandler().timestamp();
if (block.timestamp < basketTimestamp + tradingDelay) return;

BasketRange memory basketsHeld = main.basketHandler().basketsHeldBy(address(this));

if (main.basketHandler().fullyCollateralized()) {
handoutExcessAssets(erc20s);
handoutExcessAssets(erc20s, basketsHeld.bottom);
} else {
/*
* Recollateralization
Expand All @@ -97,8 +99,6 @@ contract BackingManagerP0 is TradingP0, IBackingManager {
* rToken.basketsNeeded to the current basket holdings. Haircut time.
*/

uint192 basketsHeld = main.basketHandler().basketsHeldBy(address(this));

(bool doTrade, TradeRequest memory req) = TradingLibP0.prepareRecollateralizationTrade(
this,
basketsHeld
Expand All @@ -114,13 +114,14 @@ contract BackingManagerP0 is TradingP0, IBackingManager {
tryTrade(req);
} else {
// Haircut time
compromiseBasketsNeeded();
compromiseBasketsNeeded(basketsHeld.bottom);
}
}
}

/// Send excess assets to the RSR and RToken traders
function handoutExcessAssets(IERC20[] calldata erc20s) private {
/// @param wholeBasketsHeld {BU} The number of full basket units held by the BackingManager
function handoutExcessAssets(IERC20[] calldata erc20s, uint192 wholeBasketsHeld) private {
assert(main.basketHandler().status() == CollateralStatus.SOUND);

// Special-case RSR to forward to StRSR pool
Expand All @@ -134,19 +135,18 @@ contract BackingManagerP0 is TradingP0, IBackingManager {
{
IRToken rToken = main.rToken();
needed = rToken.basketsNeeded(); // {BU}
uint192 held = main.basketHandler().basketsHeldBy(address(this)); // {BU}
if (held.gt(needed)) {
if (wholeBasketsHeld.gt(needed)) {
int8 decimals = int8(rToken.decimals());
uint192 totalSupply = shiftl_toFix(rToken.totalSupply(), -decimals); // {rTok}

// {BU} = {BU} - {BU}
uint192 extraBUs = held.minus(needed);
uint192 extraBUs = wholeBasketsHeld.minus(needed);

// {qRTok: Fix} = {BU} * {qRTok / BU} (if needed == 0, conv rate is 1 qRTok/BU)
uint192 rTok = (needed > 0) ? extraBUs.mulDiv(totalSupply, needed) : extraBUs;

rToken.mint(address(this), rTok);
rToken.setBasketsNeeded(held);
rToken.setBasketsNeeded(wholeBasketsHeld);
}
}

Expand Down Expand Up @@ -180,9 +180,10 @@ contract BackingManagerP0 is TradingP0, IBackingManager {
}

/// Compromise on how many baskets are needed in order to recollateralize-by-accounting
function compromiseBasketsNeeded() private {
/// @param wholeBasketsHeld {BU} The number of full basket units held by the BackingManager
function compromiseBasketsNeeded(uint192 wholeBasketsHeld) private {
assert(tradesOpen == 0 && !main.basketHandler().fullyCollateralized());
main.rToken().setBasketsNeeded(main.basketHandler().basketsHeldBy(address(this)));
main.rToken().setBasketsNeeded(wholeBasketsHeld);
assert(main.basketHandler().fullyCollateralized());
}

Expand Down
Loading

0 comments on commit ed6b8fe

Please sign in to comment.