Skip to content

Commit

Permalink
Trust M-03: track balances out on trade for RTokenAsset.price() (#973)
Browse files Browse the repository at this point in the history
  • Loading branch information
tbrent authored Oct 30, 2023
1 parent 6a8fae6 commit 89128e3
Show file tree
Hide file tree
Showing 24 changed files with 355 additions and 198 deletions.
4 changes: 3 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,11 @@ Upgrade all core contracts and _all_ assets. ERC20s do not need to be upgraded.

Then, call `Broker.cacheComponents()`.

Finally, call `Broker.setBatchTradeImplementation(newGnosisTrade)`.

### Core Protocol Contracts

- `BackingManager`
- `BackingManager` [+2 slots]
- Replace use of `lotPrice()` with `price()`
- `BasketHandler`
- Remove `lotPrice()`
Expand Down
37 changes: 37 additions & 0 deletions contracts/interfaces/IBackingManager.sol
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,38 @@
pragma solidity 0.8.19;

import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "./IAssetRegistry.sol";
import "./IBasketHandler.sol";
import "./IBroker.sol";
import "./IComponent.sol";
import "./IRToken.sol";
import "./IStRSR.sol";
import "./ITrading.sol";

/// Memory struct for RecollateralizationLibP1 + RTokenAsset
/// Struct purposes:
/// 1. Configure trading
/// 2. Stay under stack limit with fewer vars
/// 3. Cache information such as component addresses and basket quantities, to save on gas
struct TradingContext {
BasketRange basketsHeld; // {BU}
// basketsHeld.top is the number of partial baskets units held
// basketsHeld.bottom is the number of full basket units held

// Components
IBasketHandler bh;
IAssetRegistry ar;
IStRSR stRSR;
IERC20 rsr;
IRToken rToken;
// Gov Vars
uint192 minTradeVolume; // {UoA}
uint192 maxTradeSlippage; // {1}
// Cached values
uint192[] quantities; // {tok/BU} basket quantities
uint192[] bals; // {tok} balances in BackingManager + out on trades
}

/**
* @title IBackingManager
* @notice The BackingManager handles changes in the ERC20 balances that back an RToken.
Expand Down Expand Up @@ -48,6 +76,15 @@ interface IBackingManager is IComponent, ITrading {
/// @param erc20s The tokens to forward
/// @custom:interaction RCEI
function forwardRevenue(IERC20[] calldata erc20s) external;

/// Structs for trading
/// @param basketsHeld The number of baskets held by the BackingManager
/// @return ctx The TradingContext
/// @return reg Contents of AssetRegistry.getRegistry()
function tradingContext(BasketRange memory basketsHeld)
external
view
returns (TradingContext memory ctx, Registry memory reg);
}

interface TestIBackingManager is IBackingManager, TestITrading {
Expand Down
3 changes: 3 additions & 0 deletions contracts/interfaces/ITrade.sol
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@ interface ITrade {

function buy() external view returns (IERC20Metadata);

/// @return {tok} The sell amount of the trade, in whole tokens
function sellAmount() external view returns (uint192);

/// @return The timestamp at which the trade is projected to become settle-able
function endTime() external view returns (uint48);

Expand Down
40 changes: 39 additions & 1 deletion contracts/p0/BackingManager.sol
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ contract BackingManagerP0 is TradingP0, IBackingManager {

mapping(TradeKind => uint48) private tradeEnd; // {s} last endTime() of an auction per kind

mapping(IERC20 => uint192) private tokensOut; // {tok} token balances out in ITrades

constructor() {
ONE_BLOCK = NetworkConfigLib.blocktime();
}
Expand Down Expand Up @@ -69,6 +71,7 @@ contract BackingManagerP0 is TradingP0, IBackingManager {
returns (ITrade trade)
{
trade = super.settleTrade(sell);
delete tokensOut[trade.sell()];

// if the settler is the trade contract itself, try chaining with another rebalance()
if (_msgSender() == address(trade)) {
Expand Down Expand Up @@ -134,7 +137,8 @@ contract BackingManagerP0 is TradingP0, IBackingManager {

// Execute Trade
ITrade trade = tryTrade(kind, req, prices);
tradeEnd[kind] = trade.endTime();
tradeEnd[kind] = trade.endTime(); // {s}
tokensOut[trade.sell()] = trade.sellAmount(); // {tok}
} else {
// Haircut time
compromiseBasketsNeeded(basketsHeld.bottom);
Expand Down Expand Up @@ -205,6 +209,40 @@ contract BackingManagerP0 is TradingP0, IBackingManager {
}
}

// === View ===

/// Structs for trading
/// @param basketsHeld The number of baskets held by the BackingManager
/// @return ctx The TradingContext
/// @return reg Contents of AssetRegistry.getRegistry()
function tradingContext(BasketRange memory basketsHeld)
public
view
returns (TradingContext memory ctx, Registry memory reg)
{
reg = main.assetRegistry().getRegistry();

ctx.basketsHeld = basketsHeld;
ctx.bh = main.basketHandler();
ctx.ar = main.assetRegistry();
ctx.stRSR = main.stRSR();
ctx.rsr = main.rsr();
ctx.rToken = main.rToken();
ctx.minTradeVolume = minTradeVolume;
ctx.maxTradeSlippage = maxTradeSlippage;
ctx.quantities = new uint192[](reg.erc20s.length);
for (uint256 i = 0; i < reg.erc20s.length; ++i) {
ctx.quantities[i] = ctx.bh.quantity(reg.erc20s[i]);
}
ctx.bals = new uint192[](reg.erc20s.length);
for (uint256 i = 0; i < reg.erc20s.length; ++i) {
ctx.bals[i] = reg.assets[i].bal(address(this)) + tokensOut[reg.erc20s[i]];

// include StRSR's balance for RSR
if (reg.erc20s[i] == ctx.rsr) ctx.bals[i] += reg.assets[i].bal(address(ctx.stRSR));
}
}

// === Private ===

/// Compromise on how many baskets are needed in order to recollateralize-by-accounting
Expand Down
21 changes: 9 additions & 12 deletions contracts/p0/mixins/TradingLib.sol
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,7 @@ library TradingLibP0 {
/// 2. Stay under stack limit with fewer vars
/// 3. Cache information such as component addresses to save on gas

struct TradingContext {
struct TradingContextP0 {
BasketRange basketsHeld; // {BU}
// basketsHeld.top is the number of partial baskets units held
// basketsHeld.bottom is the number of full basket units held
Expand Down Expand Up @@ -190,7 +190,7 @@ library TradingLibP0 {
// === Prepare cached values ===

IMain main = bm.main();
TradingContext memory ctx = TradingContext({
TradingContextP0 memory ctx = TradingContextP0({
basketsHeld: basketsHeld,
bm: bm,
bh: main.basketHandler(),
Expand Down Expand Up @@ -241,14 +241,9 @@ library TradingLibP0 {
// token balances requiring trading vs not requiring trading. Seek to decrease uncertainty
// the largest amount possible with each trade.
//
// How do we know this algorithm converges?
// Assumption: constant oracle prices; monotonically increasing refPerTok()
// Any volume traded narrows the BU band. Why:
// - We might increase `basketsHeld.bottom` from run-to-run, but will never decrease it
// - We might decrease the UoA amount of excess balances beyond `basketsHeld.bottom` from
// run-to-run, but will never increase it
// - We might decrease the UoA amount of missing balances up-to `basketsHeld.top` from
// run-to-run, but will never increase it
// Algorithm Invariant: every increase of basketsHeld.bottom causes basketsRange().low to
// reach a new maximum. Note that basketRange().low may decrease slightly along the way.
// Assumptions: constant oracle prices; monotonically increasing refPerTok; no supply changes
//
// Preconditions:
// - ctx is correctly populated, with current basketsHeld.bottom + basketsHeld.top
Expand All @@ -269,7 +264,7 @@ library TradingLibP0 {
// - range.bottom = min(rToken.basketsNeeded, basketsHeld.bottom + least baskets purchaseable)
// where "least baskets purchaseable" involves trading at the worst price,
// incurring the full maxTradeSlippage, and taking up to a minTradeVolume loss due to dust.
function basketRange(TradingContext memory ctx, IERC20[] memory erc20s)
function basketRange(TradingContextP0 memory ctx, IERC20[] memory erc20s)
internal
view
returns (BasketRange memory range)
Expand Down Expand Up @@ -428,10 +423,12 @@ library TradingLibP0 {
// Sell IFFY last because it may recover value in the future.
// All collateral in the basket have already been guaranteed to be SOUND by upstream checks.
function nextTradePair(
TradingContext memory ctx,
TradingContextP0 memory ctx,
IERC20[] memory erc20s,
BasketRange memory range
) private view returns (TradeInfo memory trade) {
// assert(tradesOpen == 0); // guaranteed by BackingManager.rebalance()

MaxSurplusDeficit memory maxes;
maxes.surplusStatus = CollateralStatus.IFFY; // least-desirable sell status

Expand Down
52 changes: 47 additions & 5 deletions contracts/p1/BackingManager.sol
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,9 @@ contract BackingManagerP1 is TradingP1, IBackingManager {
IFurnace private furnace;
mapping(TradeKind => uint48) private tradeEnd; // {s} last endTime() of an auction per kind

// === 3.0.1 ===
mapping(IERC20 => uint192) private tokensOut; // {tok} token balances out in ITrades

// ==== Invariants ====
// tradingDelay <= MAX_TRADING_DELAY and backingBuffer <= MAX_BACKING_BUFFER

Expand Down Expand Up @@ -90,6 +93,7 @@ contract BackingManagerP1 is TradingP1, IBackingManager {
/// @return trade The ITrade contract settled
/// @custom:interaction
function settleTrade(IERC20 sell) public override(ITrading, TradingP1) returns (ITrade trade) {
delete tokensOut[sell];
trade = super.settleTrade(sell); // nonReentrant

// if the settler is the trade contract itself, try chaining with another rebalance()
Expand Down Expand Up @@ -148,22 +152,26 @@ contract BackingManagerP1 is TradingP1, IBackingManager {
* rToken.basketsNeeded to the current basket holdings. Haircut time.
*/

(TradingContext memory ctx, Registry memory reg) = tradingContext(basketsHeld);
(
bool doTrade,
TradeRequest memory req,
TradePrices memory prices
) = RecollateralizationLibP1.prepareRecollateralizationTrade(this, basketsHeld);
) = RecollateralizationLibP1.prepareRecollateralizationTrade(ctx, reg);

if (doTrade) {
IERC20 sellERC20 = req.sell.erc20();

// Seize RSR if needed
if (req.sell.erc20() == rsr) {
uint256 bal = req.sell.erc20().balanceOf(address(this));
if (sellERC20 == rsr) {
uint256 bal = sellERC20.balanceOf(address(this));
if (req.sellAmount > bal) stRSR.seizeRSR(req.sellAmount - bal);
}

// Execute Trade
ITrade trade = tryTrade(kind, req, prices);
tradeEnd[kind] = trade.endTime();
tradeEnd[kind] = trade.endTime(); // {s}
tokensOut[sellERC20] = trade.sellAmount(); // {tok}
} else {
// Haircut time
compromiseBasketsNeeded(basketsHeld.bottom);
Expand Down Expand Up @@ -264,6 +272,40 @@ contract BackingManagerP1 is TradingP1, IBackingManager {
// It's okay if there is leftover dust for RToken or a surplus asset (not RSR)
}

// === View ===

/// Structs for trading
/// @param basketsHeld The number of baskets held by the BackingManager
/// @return ctx The TradingContext
/// @return reg Contents of AssetRegistry.getRegistry()
function tradingContext(BasketRange memory basketsHeld)
public
view
returns (TradingContext memory ctx, Registry memory reg)
{
reg = assetRegistry.getRegistry();

ctx.basketsHeld = basketsHeld;
ctx.bh = basketHandler;
ctx.ar = assetRegistry;
ctx.stRSR = stRSR;
ctx.rsr = rsr;
ctx.rToken = rToken;
ctx.minTradeVolume = minTradeVolume;
ctx.maxTradeSlippage = maxTradeSlippage;
ctx.quantities = new uint192[](reg.erc20s.length);
for (uint256 i = 0; i < reg.erc20s.length; ++i) {
ctx.quantities[i] = basketHandler.quantityUnsafe(reg.erc20s[i], reg.assets[i]);
}
ctx.bals = new uint192[](reg.erc20s.length);
for (uint256 i = 0; i < reg.erc20s.length; ++i) {
ctx.bals[i] = reg.assets[i].bal(address(this)) + tokensOut[reg.erc20s[i]];

// include StRSR's balance for RSR
if (reg.erc20s[i] == rsr) ctx.bals[i] += reg.assets[i].bal(address(stRSR));
}
}

// === Private ===

/// Compromise on how many baskets are needed in order to recollateralize-by-accounting
Expand Down Expand Up @@ -308,5 +350,5 @@ contract BackingManagerP1 is TradingP1, IBackingManager {
* variables without shifting down storage in the inheritance chain.
* See https://docs.openzeppelin.com/contracts/4.x/upgradeable#storage_gaps
*/
uint256[39] private __gap;
uint256[37] private __gap;
}
Loading

0 comments on commit 89128e3

Please sign in to comment.