From e85ed27d8cae87119d6746fe9c1cd8ce44254552 Mon Sep 17 00:00:00 2001 From: Taylor Brent Date: Wed, 2 Aug 2023 18:28:43 +0200 Subject: [PATCH] Zero distribution (#878) --- contracts/interfaces/IRevenueTrader.sol | 4 + contracts/p0/RevenueTrader.sol | 20 ++++ contracts/p1/RevenueTrader.sol | 25 ++++- .../plugins/mocks/InvalidRevTraderP1Mock.sol | 24 +++- test/Revenues.test.ts | 106 ++++++++++++++++++ 5 files changed, 177 insertions(+), 2 deletions(-) diff --git a/contracts/interfaces/IRevenueTrader.sol b/contracts/interfaces/IRevenueTrader.sol index 4b07ff70b..8ab78078e 100644 --- a/contracts/interfaces/IRevenueTrader.sol +++ b/contracts/interfaces/IRevenueTrader.sol @@ -25,6 +25,10 @@ interface IRevenueTrader is IComponent, ITrading { /// @custom:interaction function distributeTokenToBuy() external; + /// Return registered ERC20s to the BackingManager if distribution for tokenToBuy is 0 + /// @custom:interaction + function returnTokens(IERC20[] memory erc20s) external; + /// Process some number of tokens /// If the tokenToBuy is included in erc20s, RevenueTrader will distribute it at end of the tx /// @param erc20s The ERC20s to manage; can be tokenToBuy or anything registered diff --git a/contracts/p0/RevenueTrader.sol b/contracts/p0/RevenueTrader.sol index 4196cc791..2f380927f 100644 --- a/contracts/p0/RevenueTrader.sol +++ b/contracts/p0/RevenueTrader.sol @@ -50,6 +50,26 @@ contract RevenueTraderP0 is TradingP0, IRevenueTrader { _distributeTokenToBuy(); } + /// Return registered ERC20s to the BackingManager if distribution for tokenToBuy is 0 + /// @custom:interaction + function returnTokens(IERC20[] memory erc20s) external notTradingPausedOrFrozen { + RevenueTotals memory revTotals = main.distributor().totals(); + if (tokenToBuy == main.rsr()) { + require(revTotals.rsrTotal == 0, "rsrTotal > 0"); + } else if (address(tokenToBuy) == address(main.rToken())) { + require(revTotals.rTokenTotal == 0, "rTokenTotal > 0"); + } else { + revert("invalid tokenToBuy"); + } + + // Return ERC20s to the BackingManager + for (uint256 i = 0; i < erc20s.length; i++) { + require(main.assetRegistry().isRegistered(erc20s[i]), "unregistered erc20"); + address backingManager = address(main.backingManager()); + erc20s[i].safeTransfer(backingManager, erc20s[i].balanceOf(address(this))); + } + } + /// Process some number of tokens /// @param erc20s The ERC20s to manage; can be tokenToBuy or anything registered /// @param kinds The kinds of auctions to launch: DUTCH_AUCTION | BATCH_AUCTION diff --git a/contracts/p1/RevenueTrader.sol b/contracts/p1/RevenueTrader.sol index 47123b02e..fe7b409b5 100644 --- a/contracts/p1/RevenueTrader.sol +++ b/contracts/p1/RevenueTrader.sol @@ -22,6 +22,7 @@ contract RevenueTraderP1 is TradingP1, IRevenueTrader { IBackingManager private backingManager; IFurnace private furnace; IRToken private rToken; + IERC20 private rsr; function init( IMain main_, @@ -43,6 +44,7 @@ contract RevenueTraderP1 is TradingP1, IRevenueTrader { backingManager = main.backingManager(); furnace = main.furnace(); rToken = main.rToken(); + rsr = main.rsr(); } /// Settle a single trade + distribute revenue @@ -62,6 +64,27 @@ contract RevenueTraderP1 is TradingP1, IRevenueTrader { _distributeTokenToBuy(); } + /// Return registered ERC20s to the BackingManager if distribution for tokenToBuy is 0 + /// @custom:interaction + function returnTokens(IERC20[] memory erc20s) external notTradingPausedOrFrozen { + RevenueTotals memory revTotals = distributor.totals(); + if (tokenToBuy == rsr) { + require(revTotals.rsrTotal == 0, "rsrTotal > 0"); + } else if (address(tokenToBuy) == address(rToken)) { + require(revTotals.rTokenTotal == 0, "rTokenTotal > 0"); + } else { + // untestable: tokenToBuy is always the RSR or RToken + revert("invalid tokenToBuy"); + } + + // Return ERC20s to the BackingManager + uint256 len = erc20s.length; + for (uint256 i = 0; i < len; ++i) { + require(assetRegistry.isRegistered(erc20s[i]), "unregistered erc20"); + erc20s[i].safeTransfer(address(backingManager), erc20s[i].balanceOf(address(this))); + } + } + /// Process some number of tokens /// If the tokenToBuy is included in erc20s, RevenueTrader will distribute it at end of the tx /// @param erc20s The ERC20s to manage; can be tokenToBuy or anything registered @@ -164,5 +187,5 @@ contract RevenueTraderP1 is TradingP1, IRevenueTrader { * variables without shifting down storage in the inheritance chain. * See https://docs.openzeppelin.com/contracts/4.x/upgradeable#storage_gaps */ - uint256[44] private __gap; + uint256[43] private __gap; } diff --git a/contracts/plugins/mocks/InvalidRevTraderP1Mock.sol b/contracts/plugins/mocks/InvalidRevTraderP1Mock.sol index 64b390423..ccb2b0f31 100644 --- a/contracts/plugins/mocks/InvalidRevTraderP1Mock.sol +++ b/contracts/plugins/mocks/InvalidRevTraderP1Mock.sol @@ -20,6 +20,7 @@ contract RevenueTraderP1InvalidReverts is TradingP1, IRevenueTrader { IBackingManager private backingManager; IFurnace private furnace; IRToken private rToken; + IERC20 private rsr; function init( IMain main_, @@ -35,13 +36,33 @@ contract RevenueTraderP1InvalidReverts is TradingP1, IRevenueTrader { } /// Distribute tokenToBuy to its destinations - function distributeTokenToBuy() public { + function distributeTokenToBuy() public notTradingPausedOrFrozen { uint256 bal = tokenToBuy.balanceOf(address(this)); tokenToBuy.safeApprove(address(main.distributor()), 0); tokenToBuy.safeApprove(address(main.distributor()), bal); main.distributor().distribute(tokenToBuy, bal); } + /// Return registered ERC20s to the BackingManager if distribution for tokenToBuy is 0 + /// @custom:interaction + function returnTokens(IERC20[] memory erc20s) external notTradingPausedOrFrozen { + RevenueTotals memory revTotals = distributor.totals(); + if (tokenToBuy == rsr) { + require(revTotals.rsrTotal == 0, "rsrTotal > 0"); + } else if (address(tokenToBuy) == address(rToken)) { + require(revTotals.rTokenTotal == 0, "rTokenTotal > 0"); + } else { + revert("invalid tokenToBuy"); + } + + // Return ERC20s to the BackingManager + uint256 len = erc20s.length; + for (uint256 i = 0; i < len; ++i) { + require(assetRegistry.isRegistered(erc20s[i]), "erc20 unregistered"); + erc20s[i].safeTransfer(address(backingManager), erc20s[i].balanceOf(address(this))); + } + } + /// Processes a single token; unpermissioned /// Reverts for testing purposes function manageTokens(IERC20[] memory, TradeKind[] memory) external notTradingPausedOrFrozen { @@ -55,5 +76,6 @@ contract RevenueTraderP1InvalidReverts is TradingP1, IRevenueTrader { backingManager = main.backingManager(); furnace = main.furnace(); rToken = main.rToken(); + rsr = main.rsr(); } } diff --git a/test/Revenues.test.ts b/test/Revenues.test.ts index ecf2a581a..f95ee4a89 100644 --- a/test/Revenues.test.ts +++ b/test/Revenues.test.ts @@ -636,6 +636,112 @@ describe(`Revenues - P${IMPLEMENTATION}`, () => { expect(await rsr.balanceOf(stRSR.address)).to.be.closeTo(expectedAmount, 100) }) + it('Should return tokens to BackingManager correctly - rsrTrader.returnTokens()', async () => { + // Mint tokens + await rsr.connect(owner).mint(rsrTrader.address, issueAmount) + await token0.connect(owner).mint(rsrTrader.address, issueAmount.add(1)) + await token1.connect(owner).mint(rsrTrader.address, issueAmount.add(2)) + + // Should fail when trading paused or frozen + await main.connect(owner).pauseIssuance() + await main.connect(owner).pauseTrading() + await main.connect(owner).freezeForever() + await expect( + rsrTrader.returnTokens([rsr.address, token0.address, token1.address]) + ).to.be.revertedWith('frozen or trading paused') + await main.connect(owner).unfreeze() + await expect( + rsrTrader.returnTokens([rsr.address, token0.address, token1.address]) + ).to.be.revertedWith('frozen or trading paused') + await main.connect(owner).unpauseTrading() + + // Should fail when distribution is nonzero + await expect( + rsrTrader.returnTokens([rsr.address, token0.address, token1.address]) + ).to.be.revertedWith('rsrTotal > 0') + await distributor.setDistribution(STRSR_DEST, { rTokenDist: bn('0'), rsrDist: bn('0') }) + + // Should fail for unregistered token + await assetRegistry.connect(owner).unregister(collateral1.address) + await expect( + rsrTrader.returnTokens([rsr.address, token0.address, token1.address]) + ).to.be.revertedWith('unregistered erc20') + + // Succeed on just token0 + rsr + await expectEvents(rsrTrader.returnTokens([rsr.address, token0.address]), [ + { + contract: rsr, + name: 'Transfer', + args: [rsrTrader.address, backingManager.address, issueAmount], + emitted: true, + }, + { + contract: token0, + name: 'Transfer', + args: [rsrTrader.address, backingManager.address, issueAmount.add(1)], + emitted: true, + }, + { + contract: token1, + name: 'Transfer', + emitted: false, + }, + ]) + }) + + it('Should return tokens to BackingManager correctly - rTokenTrader.returnTokens()', async () => { + // Mint tokens + await rsr.connect(owner).mint(rTokenTrader.address, issueAmount) + await token0.connect(owner).mint(rTokenTrader.address, issueAmount.add(1)) + await token1.connect(owner).mint(rTokenTrader.address, issueAmount.add(2)) + + // Should fail when trading paused or frozen + await main.connect(owner).pauseIssuance() + await main.connect(owner).pauseTrading() + await main.connect(owner).freezeForever() + await expect( + rTokenTrader.returnTokens([rsr.address, token0.address, token1.address]) + ).to.be.revertedWith('frozen or trading paused') + await main.connect(owner).unfreeze() + await expect( + rTokenTrader.returnTokens([rsr.address, token0.address, token1.address]) + ).to.be.revertedWith('frozen or trading paused') + await main.connect(owner).unpauseTrading() + + // Should fail when distribution is nonzero + await expect( + rTokenTrader.returnTokens([rsr.address, token0.address, token1.address]) + ).to.be.revertedWith('rTokenTotal > 0') + await distributor.setDistribution(FURNACE_DEST, { rTokenDist: bn('0'), rsrDist: bn('0') }) + + // Should fail for unregistered token + await assetRegistry.connect(owner).unregister(collateral1.address) + await expect( + rTokenTrader.returnTokens([rsr.address, token0.address, token1.address]) + ).to.be.revertedWith('unregistered erc20') + + // Succeed on just token0 + rsr + await expectEvents(rTokenTrader.returnTokens([rsr.address, token0.address]), [ + { + contract: rsr, + name: 'Transfer', + args: [rTokenTrader.address, backingManager.address, issueAmount], + emitted: true, + }, + { + contract: token0, + name: 'Transfer', + args: [rTokenTrader.address, backingManager.address, issueAmount.add(1)], + emitted: true, + }, + { + contract: token1, + name: 'Transfer', + emitted: false, + }, + ]) + }) + it('Should launch multiple auctions -- has tokenToBuy', async () => { // Mint AAVE, token0, and RSR to the RSRTrader await aaveToken.connect(owner).mint(rsrTrader.address, issueAmount)