Skip to content

Commit

Permalink
Revenue Facet (#1182)
Browse files Browse the repository at this point in the history
  • Loading branch information
tbrent authored Aug 9, 2024
1 parent 5b428cd commit c8752bc
Show file tree
Hide file tree
Showing 18 changed files with 476 additions and 189 deletions.
102 changes: 9 additions & 93 deletions contracts/facade/facets/ActFacet.sol
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import "@openzeppelin/contracts/utils/Multicall.sol";
import "../../plugins/trading/DutchTrade.sol";
import "../../plugins/trading/GnosisTrade.sol";
import "../../interfaces/IBackingManager.sol";
import "../lib/FacetLib.sol";

/**
* @title ActFacet
Expand Down Expand Up @@ -45,7 +46,7 @@ contract ActFacet is Multicall {
) external {
// Settle auctions
for (uint256 i = 0; i < toSettle.length; ++i) {
_settleTrade(revenueTrader, toSettle[i]);
FacetLib.settleTrade(revenueTrader, toSettle[i]);
}

// if 2.1.0, distribute tokenToBuy
Expand All @@ -59,10 +60,10 @@ contract ActFacet is Multicall {
if (toStart.length == 0) return;

// Transfer revenue backingManager -> revenueTrader
_forwardRevenue(revenueTrader.main().backingManager(), toStart);
FacetLib.forwardRevenue(revenueTrader.main().backingManager(), toStart);

// Start RevenueTrader auctions
_runRevenueAuctions(revenueTrader, toStart, kinds);
FacetLib.runRevenueAuctions(revenueTrader, toStart, kinds);
}

// === Static Calls ===
Expand Down Expand Up @@ -93,7 +94,7 @@ contract ActFacet is Multicall {
Registry memory reg = revenueTrader.main().assetRegistry().getRegistry();

// Forward ALL revenue
_forwardRevenue(bm, reg.erc20s);
FacetLib.forwardRevenue(bm, reg.erc20s);

erc20s = new IERC20[](reg.erc20s.length);
canStart = new bool[](reg.erc20s.length);
Expand All @@ -109,7 +110,7 @@ contract ActFacet is Multicall {
// Settle first if possible. Required so we can assess full available balance
ITrade trade = revenueTrader.trades(erc20s[i]);
if (address(trade) != address(0) && trade.canSettle()) {
_settleTrade(revenueTrader, erc20s[i]);
FacetLib.settleTrade(revenueTrader, erc20s[i]);
}

surpluses[i] = erc20s[i].balanceOf(address(revenueTrader));
Expand Down Expand Up @@ -175,15 +176,15 @@ contract ActFacet is Multicall {
for (uint256 i = 0; i < erc20s.length; ++i) {
ITrade trade = bm.trades(erc20s[i]);
if (address(trade) != address(0) && trade.canSettle()) {
_settleTrade(bm, erc20s[i]);
FacetLib.settleTrade(bm, erc20s[i]);
break; // backingManager can only have 1 trade open at a time
}
}
}

// If no auctions ongoing, to find a new auction to start
if (bm.tradesOpen() == 0) {
_rebalance(bm, kind);
FacetLib.rebalance(bm, kind);

// Find the started auction
for (uint256 i = 0; i < erc20s.length; ++i) {
Expand All @@ -192,95 +193,10 @@ contract ActFacet is Multicall {
canStart = true;
sell = trade.sell();
buy = trade.buy();
sellAmount = _getSellAmount(trade);
sellAmount = FacetLib.getSellAmount(trade);
}
}
}
}

// === Private ===
function _getSellAmount(ITrade trade) private view returns (uint256) {
if (trade.KIND() == TradeKind.DUTCH_AUCTION) {
return
DutchTrade(address(trade)).sellAmount().shiftl_toUint(
int8(trade.sell().decimals())
);
} else if (trade.KIND() == TradeKind.BATCH_AUCTION) {
return GnosisTrade(address(trade)).initBal();
} else {
revert("invalid trade type");
}
}

function _settleTrade(ITrading trader, IERC20 toSettle) private {
bytes1 majorVersion = bytes(trader.version())[0];
if (majorVersion == bytes1("3")) {
// Settle auctions
trader.settleTrade(toSettle);
} else if (majorVersion == bytes1("2") || majorVersion == bytes1("1")) {
address(trader).functionCall(abi.encodeWithSignature("settleTrade(address)", toSettle));
} else {
_revertUnrecognizedVersion();
}
}

function _forwardRevenue(IBackingManager bm, IERC20[] memory toStart) private {
bytes1 majorVersion = bytes(bm.version())[0];
// Need to use try-catch here in order to still show revenueOverview when basket not ready
if (majorVersion == bytes1("3")) {
// solhint-disable-next-line no-empty-blocks
try bm.forwardRevenue(toStart) {} catch {}
} else if (majorVersion == bytes1("2") || majorVersion == bytes1("1")) {
// solhint-disable-next-line avoid-low-level-calls
(bool success, ) = address(bm).call{ value: 0 }(
abi.encodeWithSignature("manageTokens(address[])", toStart)
);
success = success; // hush warning
} else {
_revertUnrecognizedVersion();
}
}

function _runRevenueAuctions(
IRevenueTrader revenueTrader,
IERC20[] memory toStart,
TradeKind[] memory kinds
) private {
bytes1 majorVersion = bytes(revenueTrader.version())[0];

if (majorVersion == bytes1("3")) {
revenueTrader.manageTokens(toStart, kinds);
} else if (majorVersion == bytes1("2") || majorVersion == bytes1("1")) {
for (uint256 i = 0; i < toStart.length; ++i) {
address(revenueTrader).functionCall(
abi.encodeWithSignature("manageToken(address)", toStart[i])
);
}
} else {
_revertUnrecognizedVersion();
}
}

function _rebalance(IBackingManager bm, TradeKind kind) private {
bytes1 majorVersion = bytes(bm.version())[0];

if (majorVersion == bytes1("3")) {
// solhint-disable-next-line no-empty-blocks
try bm.rebalance(kind) {} catch {}
} else if (majorVersion == bytes1("2") || majorVersion == bytes1("1")) {
IERC20[] memory emptyERC20s = new IERC20[](0);
// solhint-disable-next-line avoid-low-level-calls
(bool success, ) = address(bm).call{ value: 0 }(
abi.encodeWithSignature("manageTokens(address[])", emptyERC20s)
);
success = success; // hush warning
} else {
_revertUnrecognizedVersion();
}
}

function _revertUnrecognizedVersion() private pure {
revert("unrecognized version");
}
}
// slither-disable-end
1 change: 0 additions & 1 deletion contracts/facade/facets/ReadFacet.sol
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ import "../../libraries/Fixed.sol";
import "../../p1/BasketHandler.sol";
import "../../p1/RToken.sol";
import "../../p1/StRSRVotes.sol";
import "./MaxIssuableFacet.sol";

/**
* @title ReadFacet
Expand Down
118 changes: 118 additions & 0 deletions contracts/facade/facets/RevenueFacet.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
// SPDX-License-Identifier: BlueOak-1.0.0
pragma solidity 0.8.19;

import "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol";
import "../../interfaces/IAssetRegistry.sol";
import "../../interfaces/IBackingManager.sol";
import "../../interfaces/IBasketHandler.sol";
import "../../interfaces/IRToken.sol";
import "../../libraries/Fixed.sol";
import "../lib/FacetLib.sol";

/**
* @title RevenueFacet
* @notice Single-function facet to return all revenues accumulating across RTokens
* @custom:static-call - Use ethers callStatic() to get result after update; do not execute
*/
// slither-disable-start
contract RevenueFacet {
using FixLib for uint192;

// keccak256(abi.encode(uint256(keccak256("RevenueFacet")) - 1)) & ~bytes32(uint256(0xff));
bytes32 private constant REVENUE_STORAGE =
0x531d6ab467582a10938423ef5fa94c1ce844452664ec58675da73580d2c39800;

/// @custom:storage-location erc7201:RevenueFacet
struct RevenueStorage {
Revenue[] revenues;
}

struct Revenue {
IRToken rToken;
IRevenueTrader trader;
IERC20 sell;
IERC20 buy;
uint8 sellDecimals;
bool settleable; // if trader.settleTrade() can be called (if can: must, to unblock)
string symbol;
uint192 volume; // {UoA} USD value of surplus balance
uint256 balance; // {qTok} surplus balance
uint256 minTradeAmount; // {qTok} min USD value worth trading
}

// === External ===

/// Return revenues across multiple RTokens
function revenues(IRToken[] memory rTokens) external returns (Revenue[] memory _revenues) {
RevenueStorage storage $ = _getStorage();
for (uint256 i = 0; i < rTokens.length; ++i) {
IERC20 rsr = IERC20(address(rTokens[i].main().rsr()));
Registry memory reg = rTokens[i].main().assetRegistry().getRegistry();

// Forward ALL revenue
FacetLib.forwardRevenue(rTokens[i].main().backingManager(), reg.erc20s);

for (uint256 j = 0; j < reg.erc20s.length; ++j) {
IERC20Metadata erc20 = IERC20Metadata(address(reg.erc20s[j]));

(uint192 low, ) = reg.assets[j].price(); // {UoA/tok}
if (low == 0) continue;

for (uint256 traderIndex = 0; traderIndex < 2; ++traderIndex) {
IRevenueTrader trader = traderIndex == 0
? rTokens[i].main().rTokenTrader()
: rTokens[i].main().rsrTrader();

// Settle first if possible to have full available balances
bool settleable = false;
if (
address(trader.trades(erc20)) != address(0) &&
trader.trades(erc20).canSettle()
) {
settleable = true;
FacetLib.settleTrade(trader, erc20);
}

IERC20 wouldBuy;
if (address(trader.trades(erc20)) == address(0)) {
wouldBuy = traderIndex == 0 ? IERC20(address(rTokens[i])) : rsr;
}

$.revenues.push(
Revenue(
rTokens[i],
trader,
erc20,
wouldBuy,
erc20.decimals(),
settleable,
erc20.symbol(),
reg.assets[j].bal(address(trader)).mul(low, FLOOR), // volume
erc20.balanceOf(address(trader)), // balance
trader.minTradeVolume().safeDiv(low, FLOOR).shiftl_toUint(
int8(erc20.decimals())
) // minTradeAmount
)
);
}
}
}

// Empty storage queues
_revenues = new Revenue[]($.revenues.length);
for (uint256 i = $.revenues.length; i > 0; --i) {
_revenues[i - 1] = $.revenues[i - 1];
$.revenues.pop();
}
assert($.revenues.length == 0);
}

// === Private ===

function _getStorage() private pure returns (RevenueStorage storage $) {
assembly {
$.slot := REVENUE_STORAGE
}
}
}
// slither-disable-end
100 changes: 100 additions & 0 deletions contracts/facade/lib/FacetLib.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
// SPDX-License-Identifier: BlueOak-1.0.0
pragma solidity 0.8.19;

import "@openzeppelin/contracts/utils/Address.sol";
import "../../interfaces/IBackingManager.sol";
import "../../interfaces/IRevenueTrader.sol";
import "../../interfaces/ITrade.sol";
import "../../interfaces/ITrading.sol";
import "../../plugins/trading/DutchTrade.sol";
import "../../plugins/trading/GnosisTrade.sol";
import "../../libraries/Fixed.sol";

library FacetLib {
using Address for address;
using FixLib for uint192;

function getSellAmount(ITrade trade) internal view returns (uint256) {
if (trade.KIND() == TradeKind.DUTCH_AUCTION) {
return
DutchTrade(address(trade)).sellAmount().shiftl_toUint(
int8(trade.sell().decimals())
);
} else if (trade.KIND() == TradeKind.BATCH_AUCTION) {
return GnosisTrade(address(trade)).initBal();
} else {
revert("invalid trade type");
}
}

function settleTrade(ITrading trader, IERC20 toSettle) internal {
bytes1 majorVersion = bytes(trader.version())[0];
if (majorVersion == bytes1("3")) {
// Settle auctions
trader.settleTrade(toSettle);
} else if (majorVersion == bytes1("2") || majorVersion == bytes1("1")) {
address(trader).functionCall(abi.encodeWithSignature("settleTrade(address)", toSettle));
} else {
_revertUnrecognizedVersion();
}
}

function forwardRevenue(IBackingManager bm, IERC20[] memory toStart) internal {
bytes1 majorVersion = bytes(bm.version())[0];
// Need to use try-catch here in order to still show revenueOverview when basket not ready
if (majorVersion == bytes1("3")) {
// solhint-disable-next-line no-empty-blocks
try bm.forwardRevenue(toStart) {} catch {}
} else if (majorVersion == bytes1("2") || majorVersion == bytes1("1")) {
// solhint-disable-next-line avoid-low-level-calls
(bool success, ) = address(bm).call{ value: 0 }(
abi.encodeWithSignature("manageTokens(address[])", toStart)
);
success = success; // hush warning
} else {
_revertUnrecognizedVersion();
}
}

function runRevenueAuctions(
IRevenueTrader revenueTrader,
IERC20[] memory toStart,
TradeKind[] memory kinds
) internal {
bytes1 majorVersion = bytes(revenueTrader.version())[0];

if (majorVersion == bytes1("3")) {
revenueTrader.manageTokens(toStart, kinds);
} else if (majorVersion == bytes1("2") || majorVersion == bytes1("1")) {
for (uint256 i = 0; i < toStart.length; ++i) {
address(revenueTrader).functionCall(
abi.encodeWithSignature("manageToken(address)", toStart[i])
);
}
} else {
_revertUnrecognizedVersion();
}
}

function rebalance(IBackingManager bm, TradeKind kind) internal {
bytes1 majorVersion = bytes(bm.version())[0];

if (majorVersion == bytes1("3")) {
// solhint-disable-next-line no-empty-blocks
try bm.rebalance(kind) {} catch {}
} else if (majorVersion == bytes1("2") || majorVersion == bytes1("1")) {
IERC20[] memory emptyERC20s = new IERC20[](0);
// solhint-disable-next-line avoid-low-level-calls
(bool success, ) = address(bm).call{ value: 0 }(
abi.encodeWithSignature("manageTokens(address[])", emptyERC20s)
);
success = success; // hush warning
} else {
_revertUnrecognizedVersion();
}
}

function _revertUnrecognizedVersion() internal pure {
revert("unrecognized version");
}
}
Loading

0 comments on commit c8752bc

Please sign in to comment.