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

Revenue Facet #1182

Merged
merged 14 commits into from
Aug 9, 2024
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 AuctionsFacet
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably need to rename to 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();

Check warning on line 47 in contracts/facade/facets/RevenueFacet.sol

View workflow job for this annotation

GitHub Actions / Lint Checks

Variable name must be in mixedCase
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 so have full available balances
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"to have"

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 $) {

Check warning on line 112 in contracts/facade/facets/RevenueFacet.sol

View workflow job for this annotation

GitHub Actions / Lint Checks

Variable name must be in mixedCase
assembly {

Check warning on line 113 in contracts/facade/facets/RevenueFacet.sol

View workflow job for this annotation

GitHub Actions / Lint Checks

Avoid to use inline assembly. It is acceptable only in rare cases
$.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
Loading