diff --git a/src/RepoTokenList.sol b/src/RepoTokenList.sol index 9075f683..3a78d1ca 100644 --- a/src/RepoTokenList.sol +++ b/src/RepoTokenList.sol @@ -304,7 +304,6 @@ library RepoTokenList { * @notice Validates a repoToken against specific criteria * @param listData The list data * @param repoToken The repoToken to validate - * @param termController The term controller * @param asset The address of the base asset * @return redemptionTimestamp The redemption timestamp of the validated repoToken * @@ -314,14 +313,8 @@ library RepoTokenList { function validateRepoToken( RepoTokenListData storage listData, ITermRepoToken repoToken, - ITermController termController, address asset ) internal view returns (uint256 redemptionTimestamp) { - // Ensure the repo token is deployed by term - if (!termController.isTermDeployed(address(repoToken))) { - revert InvalidRepoToken(address(repoToken)); - } - // Retrieve repo token configuration address purchaseToken; address collateralManager; @@ -357,7 +350,6 @@ library RepoTokenList { * @notice Validate and insert a repoToken into the list data * @param listData The list data * @param repoToken The repoToken to validate and insert - * @param termController The term controller * @param discountRateAdapter The discount rate adapter * @param asset The address of the base asset * @return discountRate The discount rate to be applied to the validated repoToken @@ -366,7 +358,6 @@ library RepoTokenList { function validateAndInsertRepoToken( RepoTokenListData storage listData, ITermRepoToken repoToken, - ITermController termController, ITermDiscountRateAdapter discountRateAdapter, address asset ) internal returns (uint256 discountRate, uint256 redemptionTimestamp) { @@ -388,7 +379,7 @@ library RepoTokenList { } else { discountRate = discountRateAdapter.getDiscountRate(address(repoToken)); - redemptionTimestamp = validateRepoToken(listData, repoToken, termController, asset); + redemptionTimestamp = validateRepoToken(listData, repoToken, asset); insertSorted(listData, address(repoToken)); listData.discountRates[address(repoToken)] = discountRate; diff --git a/src/Strategy.sol b/src/Strategy.sol index 01fe2d7e..c3792c7a 100644 --- a/src/Strategy.sol +++ b/src/Strategy.sol @@ -45,6 +45,8 @@ contract Strategy is BaseStrategy, Pausable, ReentrancyGuard { error BalanceBelowliquidityReserveRatio(); error InsufficientLiquidBalance(uint256 have, uint256 want); error RepoTokenConcentrationTooHigh(address repoToken); + error RepoTokenBlacklisted(address repoToken); + error DepositPaused(); // Immutable state variables ITermVaultEvents public immutable TERM_VAULT_EVENT_EMITTER; @@ -52,7 +54,10 @@ contract Strategy is BaseStrategy, Pausable, ReentrancyGuard { IERC4626 public immutable YEARN_VAULT; // State variables - ITermController public termController; + /// @notice previous term controller + ITermController public prevTermController; + /// @notice current term controller + ITermController public currTermController; ITermDiscountRateAdapter public discountRateAdapter; RepoTokenListData internal repoTokenListData; TermAuctionListData internal termAuctionListData; @@ -60,6 +65,15 @@ contract Strategy is BaseStrategy, Pausable, ReentrancyGuard { uint256 public liquidityReserveRatio; // purchase token precision (underlying) uint256 public discountRateMarkup; // 1e18 (TODO: check this) uint256 public repoTokenConcentrationLimit; + mapping(address => bool) repoTokenBlacklist; + bool depositLock; + + modifier notBlacklisted(address repoToken) { + if (repoTokenBlacklist[repoToken]) { + revert RepoTokenBlacklisted(repoToken); + } + _; + } /*////////////////////////////////////////////////////////////// MANAGEMENT FUNCTIONS @@ -82,17 +96,33 @@ contract Strategy is BaseStrategy, Pausable, ReentrancyGuard { /** * @notice Pause the contract */ - function pause() external onlyManagement { + function pauseDeposit() external onlyManagement { + depositLock = true; + TERM_VAULT_EVENT_EMITTER.emitDepositPaused(); + } + + /** + * @notice Unpause the contract + */ + function unpauseDeposit() external onlyManagement { + depositLock = false; + TERM_VAULT_EVENT_EMITTER.emitDepositUnpaused(); + } + + /** + * @notice Pause the contract + */ + function pauseStrategy() external onlyManagement { _pause(); - TERM_VAULT_EVENT_EMITTER.emitPaused(); + TERM_VAULT_EVENT_EMITTER.emitStrategyPaused(); } /** * @notice Unpause the contract */ - function unpause() external onlyManagement { + function unpauseStrategy() external onlyManagement { _unpause(); - TERM_VAULT_EVENT_EMITTER.emitUnpaused(); + TERM_VAULT_EVENT_EMITTER.emitStrategyUnpaused(); } /** @@ -103,11 +133,13 @@ contract Strategy is BaseStrategy, Pausable, ReentrancyGuard { address newTermController ) external onlyManagement { require(newTermController != address(0)); + address current = address(currTermController); TERM_VAULT_EVENT_EMITTER.emitTermControllerUpdated( - address(termController), + current, newTermController ); - termController = ITermController(newTermController); + prevTermController = ITermController(current); + currTermController = ITermController(newTermController); } /** @@ -196,6 +228,11 @@ contract Strategy is BaseStrategy, Pausable, ReentrancyGuard { repoTokenListData.collateralTokenParams[tokenAddr] = minCollateralRatio; } + function setRepoTokenBlacklist(address repoToken, bool blacklisted) external onlyManagement { + TERM_VAULT_EVENT_EMITTER.emitRepoTokenBlacklistUpdated(repoToken, blacklisted); + repoTokenBlacklist[repoToken] = blacklisted; + } + /*////////////////////////////////////////////////////////////// VIEW FUNCTIONS //////////////////////////////////////////////////////////////*/ @@ -261,9 +298,12 @@ contract Strategy is BaseStrategy, Pausable, ReentrancyGuard { ) external view returns (uint256) { // do not validate if we are simulating with existing repoTokens if (repoToken != address(0)) { + if (!_isTermDeployed(repoToken)) { + revert RepoTokenList.InvalidRepoToken(address(repoToken)); + } + repoTokenListData.validateRepoToken( ITermRepoToken(repoToken), - termController, address(asset) ); @@ -526,7 +566,6 @@ contract Strategy is BaseStrategy, Pausable, ReentrancyGuard { bool foundInOfferList ) = termAuctionListData.getCumulativeOfferData( repoTokenListData, - termController, repoToken, repoTokenAmount, PURCHASE_TOKEN_PRECISION @@ -572,6 +611,16 @@ contract Strategy is BaseStrategy, Pausable, ReentrancyGuard { YEARN_VAULT.deposit(IERC20(asset).balanceOf(address(this)), address(this)); } + function _isTermDeployed(address termContract) private view returns (bool) { + if (address(currTermController) != address(0) && currTermController.isTermDeployed(termContract)) { + return true; + } + if (address(prevTermController) != address(0) && prevTermController.isTermDeployed(termContract)) { + return true; + } + return false; + } + /** * @notice Rebalances the strategy's assets by sweeping assets and redeeming matured repoTokens * @param liquidAmountRequired The amount of liquid assets required to be maintained by the strategy @@ -586,7 +635,6 @@ contract Strategy is BaseStrategy, Pausable, ReentrancyGuard { // Remove completed auction offers termAuctionListData.removeCompleted( repoTokenListData, - termController, discountRateAdapter, address(asset) ); @@ -617,6 +665,42 @@ contract Strategy is BaseStrategy, Pausable, ReentrancyGuard { STRATEGIST FUNCTIONS //////////////////////////////////////////////////////////////*/ + function _validateAndGetOfferLocker( + ITermAuction termAuction, + address repoToken, + uint256 purchaseTokenAmount + ) private view returns (ITermAuctionOfferLocker) { + // Verify that the term auction and repo token are valid and deployed by term + if (!_isTermDeployed(address(termAuction))) { + revert InvalidTermAuction(address(termAuction)); + } + if (!_isTermDeployed(repoToken)) { + revert RepoTokenList.InvalidRepoToken(address(repoToken)); + } + + require(termAuction.termRepoId() == ITermRepoToken(repoToken).termRepoId(), "repoToken does not match term repo ID"); + + // Validate purchase token, min collateral ratio and insert the repoToken if necessary + repoTokenListData.validateRepoToken( + ITermRepoToken(repoToken), + address(asset) + ); + + _validateRepoTokenConcentration(repoToken, purchaseTokenAmount, 0); + + // Prepare and submit the offer + ITermAuctionOfferLocker offerLocker = ITermAuctionOfferLocker( + termAuction.termAuctionOfferLocker() + ); + require( + block.timestamp > offerLocker.auctionStartTime() || + block.timestamp < termAuction.auctionEndTime(), + "Auction not open" + ); + + return offerLocker; + } + /** * @notice Submits an offer into a term auction for a specified repoToken * @param termAuction The address of the term auction @@ -630,7 +714,7 @@ contract Strategy is BaseStrategy, Pausable, ReentrancyGuard { * and rebalances liquidity to support the offer submission. It handles both new offers and edits to existing offers. */ function submitAuctionOffer( - address termAuction, + ITermAuction termAuction, address repoToken, bytes32 idHash, bytes32 offerPriceHash, @@ -639,36 +723,16 @@ contract Strategy is BaseStrategy, Pausable, ReentrancyGuard { external whenNotPaused nonReentrant + notBlacklisted(repoToken) onlyManagement returns (bytes32[] memory offerIds) { require(purchaseTokenAmount > 0, "Purchase token amount must be greater than zero"); - // Verify that the term auction is valid and deployed by term - if (!termController.isTermDeployed(termAuction)) { - revert InvalidTermAuction(termAuction); - } - - ITermAuction auction = ITermAuction(termAuction); - require(auction.termRepoId() == ITermRepoToken(repoToken).termRepoId(), "repoToken does not match term repo ID"); - - // Validate purchase token, min collateral ratio and insert the repoToken if necessary - repoTokenListData.validateRepoToken( - ITermRepoToken(repoToken), - termController, - address(asset) - ); - - _validateRepoTokenConcentration(repoToken, purchaseTokenAmount, 0); - - // Prepare and submit the offer - ITermAuctionOfferLocker offerLocker = ITermAuctionOfferLocker( - auction.termAuctionOfferLocker() - ); - require( - block.timestamp > offerLocker.auctionStartTime() || - block.timestamp < auction.auctionEndTime(), - "Auction not open" + ITermAuctionOfferLocker offerLocker = _validateAndGetOfferLocker( + termAuction, + repoToken, + purchaseTokenAmount ); // Sweep assets, redeem matured repoTokens and ensure liquid balances up to date @@ -732,7 +796,7 @@ contract Strategy is BaseStrategy, Pausable, ReentrancyGuard { offer.purchaseToken = address(asset); offerIds = _submitOffer( - auction, + termAuction, offerLocker, offer, repoToken, @@ -823,7 +887,7 @@ contract Strategy is BaseStrategy, Pausable, ReentrancyGuard { bytes32[] calldata offerIds ) external onlyManagement { // Validate if the term auction is deployed by term - if (!termController.isTermDeployed(termAuction)) { + if (!_isTermDeployed(termAuction)) { revert InvalidTermAuction(termAuction); } @@ -839,7 +903,6 @@ contract Strategy is BaseStrategy, Pausable, ReentrancyGuard { // Update the term auction list data and remove completed offers termAuctionListData.removeCompleted( repoTokenListData, - termController, discountRateAdapter, address(asset) ); @@ -882,15 +945,19 @@ contract Strategy is BaseStrategy, Pausable, ReentrancyGuard { function sellRepoToken( address repoToken, uint256 repoTokenAmount - ) external whenNotPaused nonReentrant { + ) external whenNotPaused nonReentrant notBlacklisted(repoToken) { // Ensure the amount of repoTokens to sell is greater than zero require(repoTokenAmount > 0); + // Make sure repo token is valid and deployed by Term + if (!_isTermDeployed(repoToken)) { + revert RepoTokenList.InvalidRepoToken(address(repoToken)); + } + // Validate and insert the repoToken into the list, retrieve auction rate and redemption timestamp (uint256 discountRate, uint256 redemptionTimestamp) = repoTokenListData .validateAndInsertRepoToken( ITermRepoToken(repoToken), - termController, discountRateAdapter, address(asset) ); @@ -1000,6 +1067,10 @@ contract Strategy is BaseStrategy, Pausable, ReentrancyGuard { * to deposit in the yield source. */ function _deployFunds(uint256 _amount) internal override whenNotPaused { + if (depositLock) { + revert DepositPaused(); + } + _sweepAsset(); _redeemRepoTokens(0); } diff --git a/src/TermAuctionList.sol b/src/TermAuctionList.sol index aa7d1f25..59b42d96 100644 --- a/src/TermAuctionList.sol +++ b/src/TermAuctionList.sol @@ -169,7 +169,6 @@ library TermAuctionList { * @notice Removes completed or cancelled offers from the list data and processes the corresponding repoTokens * @param listData The list data * @param repoTokenListData The repoToken list data - * @param termController The term controller * @param discountRateAdapter The discount rate adapter * @param asset The address of the asset * @@ -180,7 +179,6 @@ library TermAuctionList { function removeCompleted( TermAuctionListData storage listData, RepoTokenListData storage repoTokenListData, - ITermController termController, ITermDiscountRateAdapter discountRateAdapter, address asset ) internal { @@ -230,6 +228,8 @@ library TermAuctionList { } if (insertRepoToken) { + // TODO: do we need to validate termDeployed(repoToken) here? + // Auction still open => include offerAmount in totalValue // (otherwise locked purchaseToken will be missing from TV) // Auction completed but not closed => include offer.offerAmount in totalValue @@ -237,7 +237,7 @@ library TermAuctionList { // This applies if the repoToken hasn't been added to the repoTokenList // (only for new auctions, not reopenings). repoTokenListData.validateAndInsertRepoToken( - ITermRepoToken(offer.repoToken), termController, discountRateAdapter, asset + ITermRepoToken(offer.repoToken), discountRateAdapter, asset ); } @@ -317,7 +317,6 @@ library TermAuctionList { * @notice Get cumulative offer data for a specified repoToken * @param listData The list data * @param repoTokenListData The repoToken list data - * @param termController The term controller * @param repoToken The address of the repoToken (optional) * @param newOfferAmount The new offer amount for the specified repoToken * @param purchaseTokenPrecision The precision of the purchase token @@ -333,7 +332,6 @@ library TermAuctionList { function getCumulativeOfferData( TermAuctionListData storage listData, RepoTokenListData storage repoTokenListData, - ITermController termController, address repoToken, uint256 newOfferAmount, uint256 purchaseTokenPrecision diff --git a/src/TermVaultEventEmitter.sol b/src/TermVaultEventEmitter.sol index b749408e..dc0f23a2 100644 --- a/src/TermVaultEventEmitter.sol +++ b/src/TermVaultEventEmitter.sol @@ -59,12 +59,20 @@ contract TermVaultEventEmitter is Initializable, UUPSUpgradeable, AccessControlU emit RepoTokenConcentrationLimitUpdated(oldLimit, newLimit); } - function emitPaused() external onlyRole(VAULT_CONTRACT) { - emit Paused(); + function emitDepositPaused() external onlyRole(VAULT_CONTRACT) { + emit DepositPaused(); } - function emitUnpaused() external onlyRole(VAULT_CONTRACT) { - emit Unpaused(); + function emitDepositUnpaused() external onlyRole(VAULT_CONTRACT) { + emit DepositUnpaused(); + } + + function emitStrategyPaused() external onlyRole(VAULT_CONTRACT) { + emit StrategyPaused(); + } + + function emitStrategyUnpaused() external onlyRole(VAULT_CONTRACT) { + emit StrategyUnpaused(); } function emitDiscountRateAdapterUpdated( @@ -74,6 +82,10 @@ contract TermVaultEventEmitter is Initializable, UUPSUpgradeable, AccessControlU emit DiscountRateAdapterUpdated(oldAdapter, newAdapter); } + function emitRepoTokenBlacklistUpdated(address repoToken, bool blacklisted) external onlyRole(VAULT_CONTRACT) { + emit RepoTokenBlacklistUpdated(repoToken, blacklisted); + } + // ======================================================================== // = Admin =============================================================== // ======================================================================== diff --git a/src/interfaces/term/ITermVaultEvents.sol b/src/interfaces/term/ITermVaultEvents.sol index 0181b09b..6537400c 100644 --- a/src/interfaces/term/ITermVaultEvents.sol +++ b/src/interfaces/term/ITermVaultEvents.sol @@ -14,15 +14,24 @@ interface ITermVaultEvents { event RepoTokenConcentrationLimitUpdated(uint256 oldLimit, uint256 newLimit); - event Paused(); + event DepositPaused(); - event Unpaused(); + event DepositUnpaused(); + + event StrategyPaused(); + + event StrategyUnpaused(); event DiscountRateAdapterUpdated( address indexed oldAdapter, address indexed newAdapter ); + event RepoTokenBlacklistUpdated( + address indexed repoToken, + bool blacklisted + ); + function emitTermControllerUpdated(address oldController, address newController) external; function emitTimeToMaturityThresholdUpdated(uint256 oldThreshold, uint256 newThreshold) external; @@ -35,12 +44,18 @@ interface ITermVaultEvents { function emitRepoTokenConcentrationLimitUpdated(uint256 oldLimit, uint256 newLimit) external; - function emitPaused() external; + function emitDepositPaused() external; - function emitUnpaused() external; + function emitDepositUnpaused() external; + + function emitStrategyPaused() external; + + function emitStrategyUnpaused() external; function emitDiscountRateAdapterUpdated( address oldAdapter, address newAdapter ) external; + + function emitRepoTokenBlacklistUpdated(address repoToken, bool blacklisted) external; } diff --git a/src/test/TestUSDCOffers.t.sol b/src/test/TestUSDCOffers.t.sol index 8b35261a..5dfccf4a 100644 --- a/src/test/TestUSDCOffers.t.sol +++ b/src/test/TestUSDCOffers.t.sol @@ -50,12 +50,12 @@ contract TestUSDCSubmitOffer is Setup { // test: only management can submit offers vm.expectRevert("!management"); bytes32[] memory offerIds = termStrategy.submitAuctionOffer( - address(repoToken1WeekAuction), address(repoToken1Week), idHash, bytes32("test price"), offerAmount + repoToken1WeekAuction, address(repoToken1Week), idHash, bytes32("test price"), offerAmount ); vm.prank(management); offerIds = termStrategy.submitAuctionOffer( - address(repoToken1WeekAuction), address(repoToken1Week), idHash, bytes32("test price"), offerAmount + repoToken1WeekAuction, address(repoToken1Week), idHash, bytes32("test price"), offerAmount ); assertEq(offerIds.length, 1); @@ -80,7 +80,7 @@ contract TestUSDCSubmitOffer is Setup { vm.prank(management); bytes32[] memory offerIds = termStrategy.submitAuctionOffer( - address(repoToken1WeekAuction), address(repoToken1Week), idHash1, bytes32("test price"), offerAmount + repoToken1WeekAuction, address(repoToken1Week), idHash1, bytes32("test price"), offerAmount ); assertEq(termStrategy.totalLiquidBalance(), initialState.totalLiquidBalance - offerAmount); diff --git a/src/test/TestUSDCSellRepoToken.t.sol b/src/test/TestUSDCSellRepoToken.t.sol index 898aa5cc..f80ed501 100644 --- a/src/test/TestUSDCSellRepoToken.t.sol +++ b/src/test/TestUSDCSellRepoToken.t.sol @@ -303,7 +303,7 @@ contract TestUSDCSellRepoToken is Setup { vm.prank(management); termStrategy.submitAuctionOffer( - address(repoToken4WeekAuction), address(repoToken4Week), idHash, bytes32("test price"), 3e6 + repoToken4WeekAuction, address(repoToken4Week), idHash, bytes32("test price"), 3e6 ); assertEq(termStrategy.simulateWeightedTimeToMaturity(address(0), 0), 1108800); @@ -319,9 +319,11 @@ contract TestUSDCSellRepoToken is Setup { vm.prank(management); termStrategy.setTermController(address(0)); + address currentController = address(termStrategy.currTermController()); vm.prank(management); termStrategy.setTermController(address(newController)); - assertEq(address(termStrategy.termController()), address(newController)); + assertEq(address(termStrategy.currTermController()), address(newController)); + assertEq(address(termStrategy.prevTermController()), currentController); vm.expectRevert("!management"); termStrategy.setTimeToMaturityThreshold(12345); @@ -552,10 +554,10 @@ contract TestUSDCSellRepoToken is Setup { mockUSDC.mint(testDepositor, depositAmount); vm.expectRevert("!management"); - termStrategy.pause(); + termStrategy.pauseStrategy(); vm.prank(management); - termStrategy.pause(); + termStrategy.pauseStrategy(); vm.startPrank(testDepositor); mockUSDC.approve(address(termStrategy), type(uint256).max); diff --git a/src/test/TestUSDCSubmitOffer.t.sol b/src/test/TestUSDCSubmitOffer.t.sol index 26d4c70d..35a5aa1e 100644 --- a/src/test/TestUSDCSubmitOffer.t.sol +++ b/src/test/TestUSDCSubmitOffer.t.sol @@ -50,12 +50,12 @@ contract TestUSDCSubmitOffer is Setup { // test: only management can submit offers vm.expectRevert("!management"); bytes32[] memory offerIds = termStrategy.submitAuctionOffer( - address(repoToken1WeekAuction), address(repoToken1Week), idHash, bytes32("test price"), offerAmount + repoToken1WeekAuction, address(repoToken1Week), idHash, bytes32("test price"), offerAmount ); vm.prank(management); offerIds = termStrategy.submitAuctionOffer( - address(repoToken1WeekAuction), address(repoToken1Week), idHash, bytes32("test price"), offerAmount + repoToken1WeekAuction, address(repoToken1Week), idHash, bytes32("test price"), offerAmount ); assertEq(offerIds.length, 1); @@ -80,7 +80,7 @@ contract TestUSDCSubmitOffer is Setup { vm.prank(management); bytes32[] memory offerIds = termStrategy.submitAuctionOffer( - address(repoToken1WeekAuction), address(repoToken1Week), idHash1, bytes32("test price"), offerAmount + repoToken1WeekAuction, address(repoToken1Week), idHash1, bytes32("test price"), offerAmount ); assertEq(termStrategy.totalLiquidBalance(), initialState.totalLiquidBalance - offerAmount);