Skip to content

Commit

Permalink
Facade read custom redeem (#21)
Browse files Browse the repository at this point in the history
Co-authored-by: Taylor Brent <[email protected]>
  • Loading branch information
julianmrodri and tbrent authored Jul 3, 2023
1 parent 8dc74f3 commit 683c22a
Show file tree
Hide file tree
Showing 12 changed files with 253 additions and 102 deletions.
69 changes: 54 additions & 15 deletions contracts/facade/FacadeAct.sol
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,17 @@ contract FacadeAct is IFacadeAct, Multicall {
_settleTrade(revenueTrader, toSettle[i]);
}

// if 2.1.0, distribute tokenToBuy
bytes1 majorVersion = bytes(revenueTrader.version())[0];
if (
toSettle.length > 0 &&
(majorVersion == MAJOR_VERSION_2 || majorVersion == MAJOR_VERSION_1)
) {
address(revenueTrader).functionCall(
abi.encodeWithSignature("manageToken(address)", revenueTrader.tokenToBuy())
);
}

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

Expand All @@ -60,28 +71,37 @@ contract FacadeAct is IFacadeAct, Multicall {
/// Includes consideration of when to distribute the RevenueTrader tokenToBuy
/// @return erc20s The ERC20s that have auctions that can be started
/// @return canStart If the ERC20 auction can be started
/// @return surpluses {qTok} The surplus amount
/// @return surpluses {qTok} The surplus amounts currently held, ignoring reward balances
/// @return minTradeAmounts {qTok} The minimum amount worth trading
/// @return bmRewards {qTok} The amounts would be claimed by backingManager.claimRewards()
/// @return revTraderRewards {qTok} The amounts that would be claimed by trader.claimRewards()
/// @dev Note that `surpluses` + `bmRewards` + `revTraderRewards`
/// @custom:static-call
function revenueOverview(IRevenueTrader revenueTrader)
external
returns (
IERC20[] memory erc20s,
bool[] memory canStart,
uint256[] memory surpluses,
uint256[] memory minTradeAmounts
uint256[] memory minTradeAmounts,
uint256[] memory bmRewards,
uint256[] memory revTraderRewards
)
{
IBackingManager bm = revenueTrader.main().backingManager();
uint192 minTradeVolume = revenueTrader.minTradeVolume(); // {UoA}
Registry memory reg = revenueTrader.main().assetRegistry().getRegistry();

// Forward ALL revenue
_forwardRevenue(revenueTrader.main().backingManager(), reg.erc20s);
_forwardRevenue(bm, reg.erc20s);

erc20s = new IERC20[](reg.erc20s.length);
canStart = new bool[](reg.erc20s.length);
surpluses = new uint256[](reg.erc20s.length);
minTradeAmounts = new uint256[](reg.erc20s.length);
bmRewards = new uint256[](reg.erc20s.length);
revTraderRewards = new uint256[](reg.erc20s.length);

// Calculate which erc20s should have auctions started
for (uint256 i = 0; i < reg.erc20s.length; ++i) {
erc20s[i] = reg.erc20s[i];
Expand All @@ -108,6 +128,27 @@ contract FacadeAct is IFacadeAct, Multicall {
canStart[i] = true;
}
}

// Calculate rewards
// Reward counts are disjoint with `surpluses` and `canStart`
for (uint256 i = 0; i < reg.erc20s.length; ++i) {
bmRewards[i] = reg.erc20s[i].balanceOf(address(bm));
}
// solhint-disable-next-line no-empty-blocks
try bm.claimRewards() {} catch {} // same between 2.1.0 and 3.0.0
for (uint256 i = 0; i < reg.erc20s.length; ++i) {
bmRewards[i] = reg.erc20s[i].balanceOf(address(bm)) - bmRewards[i];
}
for (uint256 i = 0; i < reg.erc20s.length; ++i) {
revTraderRewards[i] = reg.erc20s[i].balanceOf(address(revenueTrader));
}
// solhint-disable-next-line no-empty-blocks
try revenueTrader.claimRewards() {} catch {} // same between 2.1.0 and 3.0.0
for (uint256 i = 0; i < reg.erc20s.length; ++i) {
revTraderRewards[i] =
reg.erc20s[i].balanceOf(address(revenueTrader)) -
revTraderRewards[i];
}
}

/// To use this, call via callStatic.
Expand Down Expand Up @@ -140,7 +181,7 @@ contract FacadeAct is IFacadeAct, Multicall {
}
}

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

Expand All @@ -165,24 +206,24 @@ contract FacadeAct is IFacadeAct, Multicall {
// Settle auctions
trader.settleTrade(toSettle);
} else if (majorVersion == MAJOR_VERSION_2 || majorVersion == MAJOR_VERSION_1) {
// solhint-disable-next-line avoid-low-level-calls
(bool success, ) = address(trader).call{ value: 0 }(
// previous versions did not return anything
abi.encodeWithSignature("settleTrade(address)", toSettle)
);
success = success; // hush warning
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 == MAJOR_VERSION_3) {
// solhint-disable-next-line no-empty-blocks
try bm.forwardRevenue(toStart) {} catch {}
} else if (majorVersion == MAJOR_VERSION_2 || majorVersion == MAJOR_VERSION_1) {
address(bm).functionCall(abi.encodeWithSignature("manageTokens(address[])", toStart));
// 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();
}
Expand All @@ -196,8 +237,7 @@ contract FacadeAct is IFacadeAct, Multicall {
bytes1 majorVersion = bytes(revenueTrader.version())[0];

if (majorVersion == MAJOR_VERSION_3) {
// solhint-disable-next-line no-empty-blocks
try revenueTrader.manageTokens(toStart, kinds) {} catch {}
revenueTrader.manageTokens(toStart, kinds);
} else if (majorVersion == MAJOR_VERSION_2 || majorVersion == MAJOR_VERSION_1) {
for (uint256 i = 0; i < toStart.length; ++i) {
address(revenueTrader).functionCall(
Expand All @@ -213,8 +253,7 @@ contract FacadeAct is IFacadeAct, Multicall {
bytes1 majorVersion = bytes(bm.version())[0];

if (majorVersion == MAJOR_VERSION_3) {
// solhint-disable-next-line no-empty-blocks
try bm.rebalance(TradeKind.DUTCH_AUCTION) {} catch {}
bm.rebalance(TradeKind.DUTCH_AUCTION);
} else if (majorVersion == MAJOR_VERSION_2 || majorVersion == MAJOR_VERSION_1) {
IERC20[] memory emptyERC20s = new IERC20[](0);
address(bm).functionCall(
Expand Down
66 changes: 53 additions & 13 deletions contracts/facade/FacadeRead.sol
Original file line number Diff line number Diff line change
Expand Up @@ -96,15 +96,16 @@ contract FacadeRead is IFacadeRead {
}

/// @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
/// @return withdrawals The balances the reedemer would receive after a full redemption
/// @return available The amount actually available, for each token
/// @dev If available[i] < withdrawals[i], then RToken.redeem() would revert
/// @custom:static-call
function redeem(IRToken rToken, uint256 amount)
external
returns (
address[] memory tokens,
uint256[] memory withdrawals,
bool isProrata
uint256[] memory available
)
{
IMain main = rToken.main();
Expand All @@ -122,23 +123,62 @@ contract FacadeRead is IFacadeRead {

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

(tokens, withdrawals) = bh.quote(basketsRedeemed, FLOOR);
available = new uint256[](tokens.length);

// 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) {
// Calculate prorata amounts
for (uint256 i = 0; i < tokens.length; i++) {
// {qTok} = {qTok} * {qRTok} / {qRTok}
uint256 prorata = mulDiv256(
IERC20Upgradeable(tokens[i]).balanceOf(backingManager),
available[i] = mulDiv256(
IERC20(tokens[i]).balanceOf(address(main.backingManager())),
amount,
supply
); // FLOOR
}
}

if (prorata < withdrawals[i]) {
withdrawals[i] = prorata;
isProrata = true;
}
/// @return tokens The erc20s returned for the redemption
/// @return withdrawals The balances necessary to issue `amount` RToken
/// @custom:static-call
function redeemCustom(
IRToken rToken,
uint256 amount,
uint48[] memory basketNonces,
uint192[] memory portions
) external returns (address[] memory tokens, uint256[] memory withdrawals) {
IMain main = rToken.main();
require(!main.frozen(), "frozen");

// Call collective state keepers.
main.poke();

uint256 supply = rToken.totalSupply();

// === Get basket redemption amounts ===
uint256 portionsSum;
for (uint256 i = 0; i < portions.length; ++i) {
portionsSum += portions[i];
}
require(portionsSum == FIX_ONE, "portions do not add up to FIX_ONE");

// D18{BU} = D18{BU} * {qRTok} / {qRTok}
uint192 basketsRedeemed = rToken.basketsNeeded().muluDivu(amount, supply);
(tokens, withdrawals) = main.basketHandler().quoteCustomRedemption(
basketNonces,
portions,
basketsRedeemed
);

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

Expand Down
39 changes: 23 additions & 16 deletions contracts/interfaces/IFacadeAct.sol
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,29 @@ interface IFacadeAct {
TradeKind[] memory kinds
) external;

// === Static Calls ===

/// To use this, call via callStatic.
/// Includes consideration of when to distribute the RevenueTrader tokenToBuy
/// @return erc20s The ERC20s that have auctions that can be started
/// @return canStart If the ERC20 auction can be started
/// @return surpluses {qTok} The surplus amounts currently held, ignoring reward balances
/// @return minTradeAmounts {qTok} The minimum amount worth trading
/// @return bmRewards {qTok} The amounts would be claimed by backingManager.claimRewards()
/// @return revTraderRewards {qTok} The amounts that would be claimed by trader.claimRewards()
/// @dev Note that `surpluses` + `bmRewards` + `revTraderRewards`
/// @custom:static-call
function revenueOverview(IRevenueTrader revenueTrader)
external
returns (
IERC20[] memory erc20s,
bool[] memory canStart,
uint256[] memory surpluses,
uint256[] memory minTradeAmounts,
uint256[] memory bmRewards,
uint256[] memory revTraderRewards
);

/// To use this, call via callStatic.
/// If canStart is true, call backingManager.rebalance(). May require settling a
/// trade first; see auctionsSettleable.
Expand All @@ -51,20 +74,4 @@ interface IFacadeAct {
IERC20 buy,
uint256 sellAmount
);

/// To use this, call via callStatic.
/// Includes consideration of when to distribute the RevenueTrader tokenToBuy
/// @return erc20s The ERC20s that have auctions that can be started
/// @return canStart If the ERC20 auction can be started
/// @return surpluses {qTok} The surplus amount
/// @return minTradeAmounts {qTok} The minimum amount worth trading
/// @custom:static-call
function revenueOverview(IRevenueTrader revenueTrader)
external
returns (
IERC20[] memory erc20s,
bool[] memory canStart,
uint256[] memory surpluses,
uint256[] memory minTradeAmounts
);
}
17 changes: 14 additions & 3 deletions contracts/interfaces/IFacadeRead.sol
Original file line number Diff line number Diff line change
Expand Up @@ -31,17 +31,28 @@ interface IFacadeRead {
);

/// @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
/// @return withdrawals The balances the reedemer would receive after a full redemption
/// @return available The amount actually available, for each token
/// @dev If available[i] < withdrawals[i], then RToken.redeem() would revert
/// @custom:static-call
function redeem(IRToken rToken, uint256 amount)
external
returns (
address[] memory tokens,
uint256[] memory withdrawals,
bool isProrata
uint256[] memory available
);

/// @return tokens The erc20s returned for the redemption
/// @return withdrawals The balances the reedemer would receive after redemption
/// @custom:static-call
function redeemCustom(
IRToken rToken,
uint256 amount,
uint48[] memory basketNonces,
uint192[] memory portions
) external returns (address[] memory tokens, uint256[] memory withdrawals);

/// @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
2 changes: 1 addition & 1 deletion contracts/interfaces/IRToken.sol
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ interface IRToken is IComponent, IERC20MetadataUpgradeable, IERC20PermitUpgradea
uint192[] memory portions,
address[] memory expectedERC20sOut,
uint256[] memory minAmounts
) external returns (address[] memory erc20sOut, uint256[] memory amountsOut);
) external;

/// Mint an amount of RToken equivalent to baskets BUs, scaling basketsNeeded up
/// Callable only by BackingManager
Expand Down
4 changes: 3 additions & 1 deletion contracts/interfaces/IRevenueTrader.sol
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,11 @@ interface IRevenueTrader is IComponent, ITrading {
/// @param kinds The kinds of auctions to launch: DUTCH_AUCTION | BATCH_AUCTION
/// @custom:interaction
function manageTokens(IERC20[] memory erc20s, TradeKind[] memory kinds) external;

function tokenToBuy() external view returns (IERC20);
}

// solhint-disable-next-line no-empty-blocks
interface TestIRevenueTrader is IRevenueTrader, TestITrading {
function tokenToBuy() external view returns (IERC20);

}
Loading

0 comments on commit 683c22a

Please sign in to comment.