From c8752bcdc5acc291941149881199e18ffadde5aa Mon Sep 17 00:00:00 2001 From: Taylor Brent Date: Fri, 9 Aug 2024 11:15:41 -0400 Subject: [PATCH] Revenue Facet (#1182) --- contracts/facade/facets/ActFacet.sol | 102 +------- contracts/facade/facets/ReadFacet.sol | 1 - contracts/facade/facets/RevenueFacet.sol | 118 +++++++++ contracts/facade/lib/FacetLib.sol | 100 ++++++++ contracts/interfaces/IFacade.sol | 4 +- scripts/addresses/1-tmp-deployments.json | 3 +- scripts/addresses/42161-tmp-deployments.json | 3 +- scripts/addresses/8453-tmp-deployments.json | 3 +- .../arbitrum-3.4.0/42161-tmp-deployments.json | 5 +- .../base-3.4.0/8453-tmp-deployments.json | 3 +- .../mainnet-3.4.0/1-tmp-deployments.json | 3 +- scripts/deploy.ts | 3 +- scripts/deployment/common.ts | 1 + .../phase1-facade/5_deploy_revenueFacet.ts | 65 +++++ scripts/verification/4_verify_facade.ts | 8 + test/Facade.test.ts | 223 +++++++++++------- test/fixtures.ts | 11 + test/integration/fixtures.ts | 9 + 18 files changed, 476 insertions(+), 189 deletions(-) create mode 100644 contracts/facade/facets/RevenueFacet.sol create mode 100644 contracts/facade/lib/FacetLib.sol create mode 100644 scripts/deployment/phase1-facade/5_deploy_revenueFacet.ts diff --git a/contracts/facade/facets/ActFacet.sol b/contracts/facade/facets/ActFacet.sol index 7294859c2e..fda4c462c8 100644 --- a/contracts/facade/facets/ActFacet.sol +++ b/contracts/facade/facets/ActFacet.sol @@ -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 @@ -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 @@ -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 === @@ -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); @@ -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)); @@ -175,7 +176,7 @@ 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 } } @@ -183,7 +184,7 @@ contract ActFacet is Multicall { // 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) { @@ -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 diff --git a/contracts/facade/facets/ReadFacet.sol b/contracts/facade/facets/ReadFacet.sol index 9924807e8f..5b7b2fe9ed 100644 --- a/contracts/facade/facets/ReadFacet.sol +++ b/contracts/facade/facets/ReadFacet.sol @@ -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 diff --git a/contracts/facade/facets/RevenueFacet.sol b/contracts/facade/facets/RevenueFacet.sol new file mode 100644 index 0000000000..b0dd976fba --- /dev/null +++ b/contracts/facade/facets/RevenueFacet.sol @@ -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 diff --git a/contracts/facade/lib/FacetLib.sol b/contracts/facade/lib/FacetLib.sol new file mode 100644 index 0000000000..4859dcd0db --- /dev/null +++ b/contracts/facade/lib/FacetLib.sol @@ -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"); + } +} diff --git a/contracts/interfaces/IFacade.sol b/contracts/interfaces/IFacade.sol index 41d947eba7..0d4549183d 100644 --- a/contracts/interfaces/IFacade.sol +++ b/contracts/interfaces/IFacade.sol @@ -5,6 +5,7 @@ import "../facade/facets/ActFacet.sol"; import "../facade/facets/ReadFacet.sol"; import "../facade/facets/BackingBufferFacet.sol"; import "../facade/facets/MaxIssuableFacet.sol"; +import "../facade/facets/RevenueFacet.sol"; interface IFacade { event SelectorSaved(address indexed facet, bytes4 indexed selector); @@ -21,7 +22,8 @@ abstract contract TestIFacade is ActFacet, BackingBufferFacet, MaxIssuableFacet, - ReadFacet + ReadFacet, + RevenueFacet { } diff --git a/scripts/addresses/1-tmp-deployments.json b/scripts/addresses/1-tmp-deployments.json index ee54814770..ae1d7c6348 100644 --- a/scripts/addresses/1-tmp-deployments.json +++ b/scripts/addresses/1-tmp-deployments.json @@ -10,7 +10,8 @@ "actFacet": "0xCAB3D3d0d5544145A6BCB47e58F61368BCcAe2dB", "readFacet": "0x823110a13eB26cB09c4Bb118DBfE4ff5f96D5526", "maxIssuableFacet": "0x5771d976696AA180Fed276FB6571fE2f41D0b849", - "backingBufferFacet": "0xB555921a031D321687aE8B0569dA7B6da8BCB209" + "backingBufferFacet": "0xB555921a031D321687aE8B0569dA7B6da8BCB209", + "revenueFacet": "0x69c21f4828c57D3BB5Eb5fEEa6C5c1432e193510" }, "facadeWriteLib": "0xDf73Cd789422040182b0C24a8b2C97bbCbba3263", "basketLib": "0xf383dC60D29A5B9ba461F40A0606870d80d1EA88", diff --git a/scripts/addresses/42161-tmp-deployments.json b/scripts/addresses/42161-tmp-deployments.json index 1fbe6d53be..90c7ca5a41 100644 --- a/scripts/addresses/42161-tmp-deployments.json +++ b/scripts/addresses/42161-tmp-deployments.json @@ -11,7 +11,8 @@ "actFacet": "0xE774CCF1431c3DEe7Fa4c20f67534b61289CAa45", "readFacet": "0x15175d35F3d88548B49600B4ee8067253A2e4e66", "backingBufferFacet": "0x73094D84683d712E02f47eddEfF70A6EDf6D59eD", - "maxIssuableFacet": "0x09108763270A8EB0D0Ca30906FEC49fa0944BFE6" + "maxIssuableFacet": "0x09108763270A8EB0D0Ca30906FEC49fa0944BFE6", + "revenueFacet": "0xA6Fa215AB89e24310dc27aD86111803C443186Eb" }, "facadeWriteLib": "0x042D85e9eb1F4372ffA362240E0630229CaA1904", "basketLib": "0x53f1Df4E5591Ae35Bf738742981669c3767241FA", diff --git a/scripts/addresses/8453-tmp-deployments.json b/scripts/addresses/8453-tmp-deployments.json index d9866bd760..5c49047817 100644 --- a/scripts/addresses/8453-tmp-deployments.json +++ b/scripts/addresses/8453-tmp-deployments.json @@ -10,7 +10,8 @@ "actFacet": "0x0eac15B9Fe585432E48Cf175571D75D111861F43", "readFacet": "0x5Af543D6F95a98200Dd770f39A902Fe793BAeB27", "maxIssuableFacet": "0x63FDcB1E8Ee5C4B64A5c4ce0FB97597917920cb6", - "backingBufferFacet": "0x38c7e9427960E427f6c84b3A096021f47a9Afb82" + "backingBufferFacet": "0x38c7e9427960E427f6c84b3A096021f47a9Afb82", + "revenueFacet": "0x4c2FCA94163355a5B81F4D924Bce8cCbACc15406" }, "facadeWriteLib": "0x186d05580E6B7195323b5dC8c3ee9179Ad086d4C", "basketLib": "0x182e86ad4a6139ced4f9fa4ed3f1cd9e4f7449e7", diff --git a/scripts/addresses/arbitrum-3.4.0/42161-tmp-deployments.json b/scripts/addresses/arbitrum-3.4.0/42161-tmp-deployments.json index 7c70ad0ba6..84e86fa98f 100644 --- a/scripts/addresses/arbitrum-3.4.0/42161-tmp-deployments.json +++ b/scripts/addresses/arbitrum-3.4.0/42161-tmp-deployments.json @@ -9,7 +9,10 @@ "facade": "0x387A0C36681A22F728ab54426356F4CAa6bB48a9", "facets": { "actFacet": "0xE774CCF1431c3DEe7Fa4c20f67534b61289CAa45", - "readFacet": "0x15175d35F3d88548B49600B4ee8067253A2e4e66" + "readFacet": "0x15175d35F3d88548B49600B4ee8067253A2e4e66", + "backingBufferFacet": "0x73094D84683d712E02f47eddEfF70A6EDf6D59eD", + "maxIssuableFacet": "0x09108763270A8EB0D0Ca30906FEC49fa0944BFE6", + "revenueFacet": "0xA6Fa215AB89e24310dc27aD86111803C443186Eb" }, "facadeWriteLib": "0x042D85e9eb1F4372ffA362240E0630229CaA1904", "basketLib": "0x53f1Df4E5591Ae35Bf738742981669c3767241FA", diff --git a/scripts/addresses/base-3.4.0/8453-tmp-deployments.json b/scripts/addresses/base-3.4.0/8453-tmp-deployments.json index f00fc82369..d8d51f4261 100644 --- a/scripts/addresses/base-3.4.0/8453-tmp-deployments.json +++ b/scripts/addresses/base-3.4.0/8453-tmp-deployments.json @@ -10,7 +10,8 @@ "actFacet": "0x0eac15B9Fe585432E48Cf175571D75D111861F43", "readFacet": "0x5Af543D6F95a98200Dd770f39A902Fe793BAeB27", "maxIssuableFacet": "0x63FDcB1E8Ee5C4B64A5c4ce0FB97597917920cb6", - "backingBufferFacet": "0x38c7e9427960E427f6c84b3A096021f47a9Afb82" + "backingBufferFacet": "0x38c7e9427960E427f6c84b3A096021f47a9Afb82", + "revenueFacet": "0x4c2FCA94163355a5B81F4D924Bce8cCbACc15406" }, "facadeWriteLib": "0x186d05580E6B7195323b5dC8c3ee9179Ad086d4C", "basketLib": "0x182e86ad4a6139ced4f9fa4ed3f1cd9e4f7449e7", diff --git a/scripts/addresses/mainnet-3.4.0/1-tmp-deployments.json b/scripts/addresses/mainnet-3.4.0/1-tmp-deployments.json index da97836adb..967cf69776 100644 --- a/scripts/addresses/mainnet-3.4.0/1-tmp-deployments.json +++ b/scripts/addresses/mainnet-3.4.0/1-tmp-deployments.json @@ -10,7 +10,8 @@ "actFacet": "0xCAB3D3d0d5544145A6BCB47e58F61368BCcAe2dB", "readFacet": "0x823110a13eB26cB09c4Bb118DBfE4ff5f96D5526", "maxIssuableFacet": "0x5771d976696AA180Fed276FB6571fE2f41D0b849", - "backingBufferFacet": "0xB555921a031D321687aE8B0569dA7B6da8BCB209" + "backingBufferFacet": "0xB555921a031D321687aE8B0569dA7B6da8BCB209", + "revenueFacet": "0x69c21f4828c57D3BB5Eb5fEEa6C5c1432e193510" }, "facadeWriteLib": "0xDf73Cd789422040182b0C24a8b2C97bbCbba3263", "basketLib": "0xf383dC60D29A5B9ba461F40A0606870d80d1EA88", diff --git a/scripts/deploy.ts b/scripts/deploy.ts index 3dc084a54b..545df26cb0 100644 --- a/scripts/deploy.ts +++ b/scripts/deploy.ts @@ -45,7 +45,8 @@ async function main() { 'phase1-facade/1_deploy_readFacet.ts', 'phase1-facade/2_deploy_actFacet.ts', 'phase1-facade/3_deploy_maxIssuable.ts', - 'phase1-facade/4_deploy_backingBufferFacet.ts' + 'phase1-facade/4_deploy_backingBufferFacet.ts', + 'phase1-facade/5_deploy_revenueFacet.ts' ) // ============================================= diff --git a/scripts/deployment/common.ts b/scripts/deployment/common.ts index 5accadc5dd..1ed7958a16 100644 --- a/scripts/deployment/common.ts +++ b/scripts/deployment/common.ts @@ -15,6 +15,7 @@ export interface IFacets { // individiual function facets maxIssuableFacet: string backingBufferFacet: string + revenueFacet: string } export interface IDeployments { diff --git a/scripts/deployment/phase1-facade/5_deploy_revenueFacet.ts b/scripts/deployment/phase1-facade/5_deploy_revenueFacet.ts new file mode 100644 index 0000000000..d50d257742 --- /dev/null +++ b/scripts/deployment/phase1-facade/5_deploy_revenueFacet.ts @@ -0,0 +1,65 @@ +import fs from 'fs' +import hre, { ethers } from 'hardhat' + +import { getChainId, isValidContract } from '../../../common/blockchain-utils' +import { networkConfig } from '../../../common/configuration' +import { getDeploymentFile, getDeploymentFilename, IDeployments } from '../common' +import { RevenueFacet } from '../../../typechain' + +let revenueFacet: RevenueFacet + +async function main() { + // ==== Read Configuration ==== + const [burner] = await hre.ethers.getSigners() + const chainId = await getChainId(hre) + + console.log(`Deploying Facade to network ${hre.network.name} (${chainId}) + with burner account: ${burner.address}`) + + if (!networkConfig[chainId]) { + throw new Error(`Missing network configuration for ${hre.network.name}`) + } + + const deploymentFilename = getDeploymentFilename(chainId) + const deployments = getDeploymentFile(deploymentFilename) + + // Check facade exists + if (!deployments.facade) { + throw new Error(`Missing deployed contracts in network ${hre.network.name}`) + } else if (!(await isValidContract(hre, deployments.facade))) { + throw new Error(`Facade contract not found in network ${hre.network.name}`) + } + + // ******************** Deploy RevenueFacet ****************************************/ + + // Deploy RevenueFacet + const RevenueFacetFactory = await ethers.getContractFactory('RevenueFacet') + revenueFacet = await RevenueFacetFactory.connect(burner).deploy() + await revenueFacet.deployed() + + // Write temporary deployments file + deployments.facets.revenueFacet = revenueFacet.address + fs.writeFileSync(deploymentFilename, JSON.stringify(deployments, null, 2)) + + console.log(`Deployed to ${hre.network.name} (${chainId}) + RevenueFacet: ${revenueFacet.address} + Deployment file: ${deploymentFilename}`) + + // ******************** Save to Facade ****************************************/ + + console.log('Configuring with Facade...') + + // Save RevenueFacet functions to Facade + const facade = await ethers.getContractAt('Facade', deployments.facade) + await facade.save( + revenueFacet.address, + Object.entries(revenueFacet.functions).map(([fn]) => revenueFacet.interface.getSighash(fn)) + ) + + console.log('Finished saving to Facade') +} + +main().catch((error) => { + console.error(error) + process.exitCode = 1 +}) diff --git a/scripts/verification/4_verify_facade.ts b/scripts/verification/4_verify_facade.ts index f1d53913a3..5fb61de951 100644 --- a/scripts/verification/4_verify_facade.ts +++ b/scripts/verification/4_verify_facade.ts @@ -54,6 +54,14 @@ async function main() { [], 'contracts/facade/facets/BackingBufferFacet.sol:BackingBufferFacet' ) + + /** ******************** Verify RevenueFacet ****************************************/ + await verifyContract( + chainId, + deployments.facets.revenueFacet, + [], + 'contracts/facade/facets/RevenueFacet.sol:RevenueFacet' + ) } main().catch((error) => { diff --git a/test/Facade.test.ts b/test/Facade.test.ts index 1b8acc7713..92a8d15607 100644 --- a/test/Facade.test.ts +++ b/test/Facade.test.ts @@ -26,6 +26,7 @@ import { ReadFacet, RecollateralizationLibP1, RevertingFacetMock, + RevenueFacet, RevenueTraderCompatibleV1, RevenueTraderCompatibleV2, RevenueTraderInvalidVersion, @@ -91,6 +92,7 @@ describe('Facade + FacadeMonitor contracts', () => { let facadeTest: FacadeTest let facadeMonitor: FacadeMonitor let readFacet: ReadFacet + let revenueFacet: RevenueFacet // Main let rToken: TestIRToken @@ -132,6 +134,7 @@ describe('Facade + FacadeMonitor contracts', () => { config, facade, readFacet, + revenueFacet, facadeTest, facadeMonitor, rToken, @@ -251,102 +254,109 @@ describe('Facade + FacadeMonitor contracts', () => { expect(await facade.stToken(rToken.address)).to.equal(stRSR.address) }) - it('Should return backingBuffer', async () => { - let [required, actual] = await facade.callStatic.backingBuffer(rToken.address) - expect(required).to.be.closeTo(fp('0.01'), fp('0.0001')) - expect(actual).to.equal(0) - - // Mimic 10% even appreciation across the board on a 0.01% backingBuffer - const [erc20s, amounts] = await basketHandler.quote(issueAmount, 0) - for (let i = 0; i < erc20s.length; i++) { - const erc20 = await ethers.getContractAt('ERC20Mock', erc20s[i]) - await erc20.connect(addr1).mint(backingManager.address, amounts[i].div(10)) - } - ;[required, actual] = await facade.callStatic.backingBuffer(rToken.address) - expect(required).to.be.closeTo(fp('0.01'), fp('0.0001')) - expect(actual).to.equal(fp('10')) // 10% - - // Add-in an uneven balance to get to 12.5% total appreciation on a 0.01% backingBuffer - await token.connect(addr1).mint(backingManager.address, issueAmount.div(4).div(10)) - ;[required, actual] = await facade.callStatic.backingBuffer(rToken.address) - expect(required).to.be.closeTo(fp('0.01'), fp('0.0001')) - expect(actual).to.equal(fp('12.5')) // 12.5% - }) - - it('Should return maxIssuable correctly', async () => { - // Regression test - // April 2nd 2024 -- maxIssuableByAmounts did not account for appreciation - // Cause RToken appreciation first to ensure basketsNeeded != totalSupply - const meltAmt = issueAmount.div(10) - const furnaceAddr = await main.furnace() - await rToken.connect(addr1).transfer(furnaceAddr, meltAmt) - await whileImpersonating(furnaceAddr, async (furnaceSigner) => { - await rToken.connect(furnaceSigner).melt(meltAmt) + context('BackingBufferFacet', () => { + it('Should return backingBuffer', async () => { + let [required, actual] = await facade.callStatic.backingBuffer(rToken.address) + expect(required).to.be.closeTo(fp('0.01'), fp('0.0001')) + expect(actual).to.equal(0) + + // Mimic 10% even appreciation across the board on a 0.01% backingBuffer + const [erc20s, amounts] = await basketHandler.quote(issueAmount, 0) + for (let i = 0; i < erc20s.length; i++) { + const erc20 = await ethers.getContractAt('ERC20Mock', erc20s[i]) + await erc20.connect(addr1).mint(backingManager.address, amounts[i].div(10)) + } + ;[required, actual] = await facade.callStatic.backingBuffer(rToken.address) + expect(required).to.be.closeTo(fp('0.01'), fp('0.0001')) + expect(actual).to.equal(fp('10')) // 10% + + // Add-in an uneven balance to get to 12.5% total appreciation on a 0.01% backingBuffer + await token.connect(addr1).mint(backingManager.address, issueAmount.div(4).div(10)) + ;[required, actual] = await facade.callStatic.backingBuffer(rToken.address) + expect(required).to.be.closeTo(fp('0.01'), fp('0.0001')) + expect(actual).to.equal(fp('12.5')) // 12.5% }) + }) - // Check values -- must reflect 10% appreciation - expect(await facade.callStatic.maxIssuable(rToken.address, addr1.address)).to.equal( - bn('3.599999991e28') - ) - expect(await facade.callStatic.maxIssuable(rToken.address, addr2.address)).to.equal( - bn('3.6e28') - ) - expect(await facade.callStatic.maxIssuable(rToken.address, other.address)).to.equal(0) + context('MaxIssuableFacet', () => { + it('Should return maxIssuable correctly', async () => { + // Regression test + // April 2nd 2024 -- maxIssuableByAmounts did not account for appreciation + // Cause RToken appreciation first to ensure basketsNeeded != totalSupply + const meltAmt = issueAmount.div(10) + const furnaceAddr = await main.furnace() + await rToken.connect(addr1).transfer(furnaceAddr, meltAmt) + await whileImpersonating(furnaceAddr, async (furnaceSigner) => { + await rToken.connect(furnaceSigner).melt(meltAmt) + }) - // Redeem all RTokens - await rToken.connect(addr1).redeem(await rToken.totalSupply()) - expect(await rToken.totalSupply()).to.equal(0) - expect(await rToken.basketsNeeded()).to.equal(0) + // Check values -- must reflect 10% appreciation + expect(await facade.callStatic.maxIssuable(rToken.address, addr1.address)).to.equal( + bn('3.599999991e28') + ) + expect(await facade.callStatic.maxIssuable(rToken.address, addr2.address)).to.equal( + bn('3.6e28') + ) + expect(await facade.callStatic.maxIssuable(rToken.address, other.address)).to.equal(0) - // With 0 baskets needed - Returns correct value at 1:1 rate, without the 10% - expect(await facade.callStatic.maxIssuable(rToken.address, addr2.address)).to.equal( - bn('4e28') - ) - }) + // Redeem all RTokens + await rToken.connect(addr1).redeem(await rToken.totalSupply()) + expect(await rToken.totalSupply()).to.equal(0) + expect(await rToken.basketsNeeded()).to.equal(0) - it('Should return maxIssuableByAmounts correctly', async () => { - const [erc20Addrs] = await basketHandler.quote(fp('1'), 0) - const erc20s = await Promise.all(erc20Addrs.map((a) => ethers.getContractAt('ERC20Mock', a))) - const addr1Amounts = await Promise.all(erc20s.map((e) => e.balanceOf(addr1.address))) - const addr2Amounts = await Promise.all(erc20s.map((e) => e.balanceOf(addr2.address))) - const otherAmounts = await Promise.all(erc20s.map((e) => e.balanceOf(other.address))) - - // Regression test - // April 2nd 2024 -- maxIssuableByAmounts did not account for appreciation - // Cause RToken appreciation first to ensure basketsNeeded != totalSupply - const meltAmt = issueAmount.div(10) - const furnaceAddr = await main.furnace() - await rToken.connect(addr1).transfer(furnaceAddr, meltAmt) - await whileImpersonating(furnaceAddr, async (furnaceSigner) => { - await rToken.connect(furnaceSigner).melt(meltAmt) + // With 0 baskets needed - Returns correct value at 1:1 rate, without the 10% + expect(await facade.callStatic.maxIssuable(rToken.address, addr2.address)).to.equal( + bn('4e28') + ) }) + it('Should return maxIssuableByAmounts correctly', async () => { + const [erc20Addrs] = await basketHandler.quote(fp('1'), 0) + const erc20s = await Promise.all( + erc20Addrs.map((a) => ethers.getContractAt('ERC20Mock', a)) + ) + const addr1Amounts = await Promise.all(erc20s.map((e) => e.balanceOf(addr1.address))) + const addr2Amounts = await Promise.all(erc20s.map((e) => e.balanceOf(addr2.address))) + const otherAmounts = await Promise.all(erc20s.map((e) => e.balanceOf(other.address))) + + // Regression test + // April 2nd 2024 -- maxIssuableByAmounts did not account for appreciation + // Cause RToken appreciation first to ensure basketsNeeded != totalSupply + const meltAmt = issueAmount.div(10) + const furnaceAddr = await main.furnace() + await rToken.connect(addr1).transfer(furnaceAddr, meltAmt) + await whileImpersonating(furnaceAddr, async (furnaceSigner) => { + await rToken.connect(furnaceSigner).melt(meltAmt) + }) - // Check values -- must reflect 10% appreciation - expect(await facade.callStatic.maxIssuableByAmounts(rToken.address, addr1Amounts)).to.equal( - bn('3.599999991e28') - ) - expect(await facade.callStatic.maxIssuableByAmounts(rToken.address, addr2Amounts)).to.equal( - bn('3.6e28') - ) - expect(await facade.callStatic.maxIssuableByAmounts(rToken.address, otherAmounts)).to.equal(0) + // Check values -- must reflect 10% appreciation + expect(await facade.callStatic.maxIssuableByAmounts(rToken.address, addr1Amounts)).to.equal( + bn('3.599999991e28') + ) + expect(await facade.callStatic.maxIssuableByAmounts(rToken.address, addr2Amounts)).to.equal( + bn('3.6e28') + ) + expect(await facade.callStatic.maxIssuableByAmounts(rToken.address, otherAmounts)).to.equal( + 0 + ) - // Redeem all RTokens - await rToken.connect(addr1).redeem(await rToken.totalSupply()) - expect(await rToken.totalSupply()).to.equal(0) - expect(await rToken.basketsNeeded()).to.equal(0) - const newAddr2Amounts = await Promise.all(erc20s.map((e) => e.balanceOf(addr2.address))) - - // With 0 baskets needed - Returns correct value at 1:1 rate, without the 10% - expect( - await facade.callStatic.maxIssuableByAmounts(rToken.address, newAddr2Amounts) - ).to.equal(bn('4e28')) - }) + // Redeem all RTokens + await rToken.connect(addr1).redeem(await rToken.totalSupply()) + expect(await rToken.totalSupply()).to.equal(0) + expect(await rToken.basketsNeeded()).to.equal(0) + const newAddr2Amounts = await Promise.all(erc20s.map((e) => e.balanceOf(addr2.address))) - it('Should revert maxIssuable when frozen', async () => { - await main.connect(owner).freezeShort() - await expect(facade.callStatic.maxIssuable(rToken.address, addr1.address)).to.be.revertedWith( - 'frozen' - ) + // With 0 baskets needed - Returns correct value at 1:1 rate, without the 10% + expect( + await facade.callStatic.maxIssuableByAmounts(rToken.address, newAddr2Amounts) + ).to.equal(bn('4e28')) + }) + + it('Should revert maxIssuable when frozen', async () => { + await main.connect(owner).freezeShort() + await expect( + facade.callStatic.maxIssuable(rToken.address, addr1.address) + ).to.be.revertedWith('frozen') + }) }) it('Should return issuable quantities correctly', async () => { @@ -773,6 +783,45 @@ describe('Facade + FacadeMonitor contracts', () => { expect(canStart).to.eql(Array(8).fill(false)) }) + context('RevenueFacet', () => { + it('Should return multiple revenues', async () => { + const rsrTraderAmt = bn('0.6e18') + const rTokenTraderAmt = bn('0.4e18') + await token.connect(addr1).transfer(rsrTrader.address, rsrTraderAmt) + await token.connect(addr1).transfer(rTokenTrader.address, rTokenTraderAmt) + + const revenues = await facade.callStatic.revenues([rToken.address, rToken.address]) // re-use same RToken since facade does not check for uniqueness + const minTradeVolume = await backingManager.minTradeVolume() // same for revenue traders + + // Check surpluses + for (let i = 0; i < revenues.length; i++) { + expect(revenues[i].rToken).to.equal(rToken.address) + if (i == 8 || i == 24) { + expect(revenues[i].trader).to.equal(rTokenTrader.address) + expect(revenues[i].buy).to.equal(rToken.address) + expect(revenues[i].symbol).to.equal('DAI') + expect(revenues[i].sellDecimals).to.equal(18) + expect(revenues[i].volume).to.equal(rTokenTraderAmt.mul(99).div(100)) + expect(revenues[i].balance).to.equal(rTokenTraderAmt) + expect(revenues[i].minTradeAmount).to.equal(minTradeVolume.mul(100).div(99)) + } else if (i == 9 || i == 25) { + expect(revenues[i].trader).to.equal(rsrTrader.address) + expect(revenues[i].buy).to.equal(rsr.address) + expect(revenues[i].symbol).to.equal('DAI') + expect(revenues[i].sellDecimals).to.equal(18) + expect(revenues[i].volume).to.equal(rsrTraderAmt.mul(99).div(100)) + expect(revenues[i].balance).to.equal(rsrTraderAmt) + expect(revenues[i].minTradeAmount).to.equal(minTradeVolume.mul(100).div(99)) + } else { + expect(revenues[i].sellDecimals).to.not.equal(0) + expect(revenues[i].volume).to.equal(0) + expect(revenues[i].balance).to.equal(0) + expect(revenues[i].minTradeAmount).to.not.equal(0) + } + } + }) + }) + itP1('Should handle invalid versions when running revenueOverview', async () => { // Use P1 specific versions rsrTrader = await ethers.getContractAt('RevenueTraderP1', rsrTrader.address) diff --git a/test/fixtures.ts b/test/fixtures.ts index af59ef1a8d..f561e9ea42 100644 --- a/test/fixtures.ts +++ b/test/fixtures.ts @@ -36,6 +36,7 @@ import { DeployerP1, DutchTrade, ReadFacet, + RevenueFacet, ActFacet, FacadeMonitor, FacadeTest, @@ -415,6 +416,7 @@ export interface DefaultFixture extends RSRAndCompAaveAndCollateralAndModuleFixt actFacet: ActFacet maxIssuableFacet: MaxIssuableFacet backingBufferFacet: BackingBufferFacet + revenueFacet: RevenueFacet facadeTest: FacadeTest facadeMonitor: FacadeMonitor broker: TestIBroker @@ -779,6 +781,14 @@ const makeDefaultFixture = async (setBasket: boolean): Promise = ) ) + // Save RevenueFacet to Facade + const RevenueFacetFactory: ContractFactory = await ethers.getContractFactory('RevenueFacet') + const revenueFacet = await RevenueFacetFactory.deploy() + await facade.save( + revenueFacet.address, + Object.entries(revenueFacet.functions).map(([fn]) => revenueFacet.interface.getSighash(fn)) + ) + return { rsr, rsrAsset, @@ -812,6 +822,7 @@ const makeDefaultFixture = async (setBasket: boolean): Promise = actFacet, maxIssuableFacet, backingBufferFacet, + revenueFacet, facadeTest, facadeMonitor, rsrTrader, diff --git a/test/integration/fixtures.ts b/test/integration/fixtures.ts index a84e69b12a..497042cf30 100644 --- a/test/integration/fixtures.ts +++ b/test/integration/fixtures.ts @@ -45,6 +45,7 @@ import { MaxIssuableFacet, NonFiatCollateral, ReadFacet, + RevenueFacet, RevenueTraderP1, RTokenAsset, RTokenP1, @@ -710,6 +711,14 @@ const makeDefaultFixture = async (setBasket: boolean): Promise = ) ) + // Save RevenueFacet to Facade + const RevenueFacetFactory: ContractFactory = await ethers.getContractFactory('RevenueFacet') + const revenueFacet = await RevenueFacetFactory.deploy() + await facade.save( + revenueFacet.address, + Object.entries(revenueFacet.functions).map(([fn]) => revenueFacet.interface.getSighash(fn)) + ) + // Deploy FacadeTest const FacadeTestFactory: ContractFactory = await ethers.getContractFactory('FacadeTest') const facadeTest = await FacadeTestFactory.deploy()