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-03: track balances out on trade for RTokenAsset.price() #973

Merged
merged 21 commits into from
Oct 30, 2023
Merged
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
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);
pmckelvy1 marked this conversation as resolved.
Show resolved Hide resolved
}

// 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