diff --git a/src/RepoTokenList.sol b/src/RepoTokenList.sol index f553b1ef..1c586604 100644 --- a/src/RepoTokenList.sol +++ b/src/RepoTokenList.sol @@ -326,7 +326,7 @@ library RepoTokenList { (redemptionTimestamp, purchaseToken, , collateralManager) = repoToken.config(); // Validate purchase token - if (purchaseToken != address(asset)) { + if (purchaseToken != asset) { revert InvalidRepoToken(address(repoToken)); } diff --git a/src/RepoTokenUtils.sol b/src/RepoTokenUtils.sol index ae2d8417..ee9dde0f 100644 --- a/src/RepoTokenUtils.sol +++ b/src/RepoTokenUtils.sol @@ -12,40 +12,6 @@ library RepoTokenUtils { uint256 public constant THREESIXTY_DAYCOUNT_SECONDS = 360 days; uint256 public constant RATE_PRECISION = 1e18; - /*////////////////////////////////////////////////////////////// - PURE FUNCTIONS - //////////////////////////////////////////////////////////////*/ - - /** - * @notice Convert repoToken amount to purchase token precision - * @param repoTokenPrecision The precision of the repoToken - * @param purchaseTokenPrecision The precision of the purchase token - * @param purchaseTokenAmountInRepoPrecision The amount of purchase token in repoToken precision - * @return The amount in purchase token precision - */ - function repoToPurchasePrecision( - uint256 repoTokenPrecision, - uint256 purchaseTokenPrecision, - uint256 purchaseTokenAmountInRepoPrecision - ) internal pure returns (uint256) { - return (purchaseTokenAmountInRepoPrecision * purchaseTokenPrecision) / repoTokenPrecision; - } - - /** - * @notice Convert purchase token amount to repoToken precision - * @param repoTokenPrecision The precision of the repoToken - * @param purchaseTokenPrecision The precision of the purchase token - * @param repoTokenAmount The amount of repoToken - * @return The amount in repoToken precision - */ - function purchaseToRepoPrecision( - uint256 repoTokenPrecision, - uint256 purchaseTokenPrecision, - uint256 repoTokenAmount - ) internal pure returns (uint256) { - return (repoTokenAmount * repoTokenPrecision) / purchaseTokenPrecision; - } - /*////////////////////////////////////////////////////////////// VIEW FUNCTIONS //////////////////////////////////////////////////////////////*/ diff --git a/src/Strategy.sol b/src/Strategy.sol index fa65baf7..fd7ccf78 100644 --- a/src/Strategy.sol +++ b/src/Strategy.sol @@ -53,6 +53,7 @@ contract Strategy is BaseStrategy, Pausable, ReentrancyGuard { IERC4626 public immutable YEARN_VAULT; /// @notice State variables + bool public depositLock; /// @dev Previous term controller ITermController public prevTermController; /// @dev Current term controller @@ -62,10 +63,9 @@ contract Strategy is BaseStrategy, Pausable, ReentrancyGuard { TermAuctionListData internal termAuctionListData; uint256 public timeToMaturityThreshold; // seconds uint256 public requiredReserveRatio; // 1e18 - uint256 public discountRateMarkup; // 1e18 (TODO: check this) + uint256 public discountRateMarkup; // 1e18 uint256 public repoTokenConcentrationLimit; // 1e18 mapping(address => bool) public repoTokenBlacklist; - bool public depositLock; modifier notBlacklisted(address repoToken) { if (repoTokenBlacklist[repoToken]) { @@ -344,7 +344,7 @@ contract Strategy is BaseStrategy, Pausable, ReentrancyGuard { uint256 proceeds; if (repoToken != address(0)) { if (!_isTermDeployed(repoToken)) { - revert RepoTokenList.InvalidRepoToken(address(repoToken)); + revert RepoTokenList.InvalidRepoToken(repoToken); } uint256 redemptionTimestamp = repoTokenListData.validateRepoToken( @@ -658,16 +658,6 @@ contract Strategy is BaseStrategy, Pausable, ReentrancyGuard { (cumulativeAmount + liquidBalance); } - /** - * @notice Deposits all available asset tokens into the liquid vault - * - * @dev This function transfers the entire balance of the asset token held by this contract - * into the associated liquid vault. - */ - function _sweepAsset() private { - YEARN_VAULT.deposit(IERC20(asset).balanceOf(address(this)), address(this)); - } - /** * @notice Checks if a term contract is marked as deployed in either the current or previous term controller * @param termContract The address of the term contract to check @@ -695,36 +685,26 @@ contract Strategy is BaseStrategy, Pausable, ReentrancyGuard { * optimizing asset allocation. */ function _redeemRepoTokens(uint256 liquidAmountRequired) private { - uint256 liquidityBefore = IERC20(asset).balanceOf(address(this)); - // Remove completed auction offers - termAuctionListData.removeCompleted( - repoTokenListData, - discountRateAdapter, - address(asset) - ); + termAuctionListData.removeCompleted(repoTokenListData, discountRateAdapter, address(asset)); // Remove and redeem matured repoTokens repoTokenListData.removeAndRedeemMaturedTokens(); - uint256 liquidityAfter = IERC20(asset).balanceOf(address(this)); - uint256 liquidityDiff = liquidityAfter - liquidityBefore; + uint256 liquidity = IERC20(asset).balanceOf(address(this)); // Deposit excess underlying balance into Yearn Vault - if (liquidityDiff > liquidAmountRequired) { + if (liquidity > liquidAmountRequired) { unchecked { - YEARN_VAULT.deposit( - liquidityDiff - liquidAmountRequired, - address(this) - ); + YEARN_VAULT.deposit(liquidity - liquidAmountRequired, address(this)); } - // Withdraw shortfall from Yearn Vault to meet required liquidity - } else if (liquidityDiff < liquidAmountRequired) { + // Withdraw shortfall from Yearn Vault to meet required liquidity + } else if (liquidity < liquidAmountRequired) { unchecked { - _withdrawAsset(liquidAmountRequired - liquidityDiff); + _withdrawAsset(liquidAmountRequired - liquidity); } } - } +} /*////////////////////////////////////////////////////////////// STRATEGIST FUNCTIONS @@ -749,7 +729,7 @@ contract Strategy is BaseStrategy, Pausable, ReentrancyGuard { revert InvalidTermAuction(address(termAuction)); } if (!_isTermDeployed(repoToken)) { - revert RepoTokenList.InvalidRepoToken(address(repoToken)); + revert RepoTokenList.InvalidRepoToken(repoToken); } require(termAuction.termRepoId() == ITermRepoToken(repoToken).termRepoId(), "repoToken does not match term repo ID"); @@ -807,7 +787,6 @@ contract Strategy is BaseStrategy, Pausable, ReentrancyGuard { ); // Sweep assets, redeem matured repoTokens and ensure liquid balances up to date - _sweepAsset(); _redeemRepoTokens(0); bytes32 offerId = _generateOfferId(idHash, address(offerLocker)); @@ -836,9 +815,12 @@ contract Strategy is BaseStrategy, Pausable, ReentrancyGuard { // Retrieve the total liquid balance uint256 liquidBalance = _totalLiquidBalance(); + uint256 totalAssetValue = _totalAssetValue(liquidBalance); + require(totalAssetValue > 0); + uint256 liquidReserveRatio = liquidBalance * 1e18 / totalAssetValue; // NOTE: we require totalAssetValue > 0 above // Check that new offer does not violate reserve ratio constraint - if (_liquidReserveRatio(liquidBalance) < requiredReserveRatio) { + if (liquidReserveRatio < requiredReserveRatio) { revert BalanceBelowRequiredReserveRatio(); } @@ -856,7 +838,7 @@ contract Strategy is BaseStrategy, Pausable, ReentrancyGuard { } // Passing in 0 amount and 0 liquid balance adjustment because offer and balance already updated - _validateRepoTokenConcentration(repoToken, 0, _totalAssetValue(liquidBalance), 0); + _validateRepoTokenConcentration(repoToken, 0, totalAssetValue, 0); } /** @@ -931,6 +913,11 @@ contract Strategy is BaseStrategy, Pausable, ReentrancyGuard { PendingOffer storage pendingOffer = termAuctionListData.offers[offerIds[0]]; pendingOffer.offerAmount = offer.amount; } + + if (newOfferAmount < currentOfferAmount) { + YEARN_VAULT.deposit(IERC20(asset).balanceOf(address(this)), address(this)); + } + } /** @@ -964,7 +951,6 @@ contract Strategy is BaseStrategy, Pausable, ReentrancyGuard { ); // Sweep any remaining assets and redeem repoTokens - _sweepAsset(); _redeemRepoTokens(0); } @@ -982,10 +968,9 @@ contract Strategy is BaseStrategy, Pausable, ReentrancyGuard { } /** - * @notice Close the auction + * @notice Required for post-processing after auction clos */ function auctionClosed() external { - _sweepAsset(); _redeemRepoTokens(0); } @@ -1007,7 +992,7 @@ contract Strategy is BaseStrategy, Pausable, ReentrancyGuard { // Make sure repo token is valid and deployed by Term if (!_isTermDeployed(repoToken)) { - revert RepoTokenList.InvalidRepoToken(address(repoToken)); + revert RepoTokenList.InvalidRepoToken(repoToken); } // Validate and insert the repoToken into the list, retrieve auction rate and redemption timestamp @@ -1019,12 +1004,13 @@ contract Strategy is BaseStrategy, Pausable, ReentrancyGuard { ); // Sweep assets and redeem repoTokens, if needed - _sweepAsset(); _redeemRepoTokens(0); - // Retrieve total liquid balance and ensure it's greater than zero + // Retrieve total asset value and liquid balance and ensure they are greater than zero uint256 liquidBalance = _totalLiquidBalance(); require(liquidBalance > 0); + uint256 totalAssetValue = _totalAssetValue(liquidBalance); + require(totalAssetValue > 0); uint256 discountRate = discountRateAdapter.getDiscountRate(repoToken); @@ -1060,8 +1046,8 @@ contract Strategy is BaseStrategy, Pausable, ReentrancyGuard { } // Ensure the remaining liquid balance is above the liquidity threshold - uint256 newLiquidBalance = liquidBalance - proceeds; - if (_liquidReserveRatio(newLiquidBalance) < requiredReserveRatio) { + uint256 newLiquidReserveRatio = ( liquidBalance - proceeds ) * 1e18 / totalAssetValue; // NOTE: we require totalAssetValue > 0 above + if (newLiquidReserveRatio < requiredReserveRatio) { revert BalanceBelowRequiredReserveRatio(); } @@ -1069,7 +1055,7 @@ contract Strategy is BaseStrategy, Pausable, ReentrancyGuard { _validateRepoTokenConcentration( repoToken, repoTokenAmountInBaseAssetPrecision, - _totalAssetValue(liquidBalance), + totalAssetValue, proceeds ); @@ -1109,6 +1095,11 @@ contract Strategy is BaseStrategy, Pausable, ReentrancyGuard { discountRateAdapter = ITermDiscountRateAdapter(_discountRateAdapter); IERC20(_asset).safeApprove(_yearnVault, type(uint256).max); + + timeToMaturityThreshold = 45 days; + requiredReserveRatio = 0.2e18; + discountRateMarkup = 0.005e18; + repoTokenConcentrationLimit = 0.1e18; } /*////////////////////////////////////////////////////////////// @@ -1131,7 +1122,6 @@ contract Strategy is BaseStrategy, Pausable, ReentrancyGuard { revert DepositPaused(); } - _sweepAsset(); _redeemRepoTokens(0); } @@ -1188,7 +1178,6 @@ contract Strategy is BaseStrategy, Pausable, ReentrancyGuard { whenNotPaused returns (uint256 _totalAssets) { - _sweepAsset(); _redeemRepoTokens(0); return _totalAssetValue(_totalLiquidBalance()); } @@ -1221,38 +1210,6 @@ contract Strategy is BaseStrategy, Pausable, ReentrancyGuard { return _totalLiquidBalance(); } - /** - * @notice Gets the max amount of `asset` that an address can deposit. - * @dev Defaults to an unlimited amount for any address. But can - * be overridden by strategists. - * - * This function will be called before any deposit or mints to enforce - * any limits desired by the strategist. This can be used for either a - * traditional deposit limit or for implementing a whitelist etc. - * - * EX: - * if(isAllowed[_owner]) return super.availableDepositLimit(_owner); - * - * This does not need to take into account any conversion rates - * from shares to assets. But should know that any non max uint256 - * amounts may be converted to shares. So it is recommended to keep - * custom amounts low enough as not to cause overflow when multiplied - * by `totalSupply`. - * - * @param . The address that is depositing into the strategy. - * @return . The available amount the `_owner` can deposit in terms of `asset` - * - function availableDepositLimit( - address _owner - ) public view override returns (uint256) { - TODO: If desired Implement deposit limit logic and any needed state variables . - - EX: - uint256 totalAssets = TokenizedStrategy.totalAssets(); - return totalAssets >= depositLimit ? 0 : depositLimit - totalAssets; - } - */ - /** * @dev Optional function for strategist to override that can * be called in between reports. @@ -1308,12 +1265,9 @@ contract Strategy is BaseStrategy, Pausable, ReentrancyGuard { * @param _amount The amount of asset to attempt to free. * function _emergencyWithdraw(uint256 _amount) internal override { - TODO: If desired implement simple logic to free deployed funds. - EX: _amount = min(_amount, aToken.balanceOf(address(this))); _freeFunds(_amount); } - */ } diff --git a/src/TermAuctionList.sol b/src/TermAuctionList.sol index e0e8fd04..7a4554b8 100644 --- a/src/TermAuctionList.sol +++ b/src/TermAuctionList.sol @@ -210,7 +210,7 @@ library TermAuctionList { removeNode = true; bytes32[] memory offerIds = new bytes32[](1); offerIds[0] = current; - offer.offerLocker.unlockOffers(offerIds); // unlocking offer in this scenario withdraws offer ammount + offer.offerLocker.unlockOffers(offerIds); // unlocking offer in this scenario withdraws offer amount } } @@ -226,7 +226,6 @@ 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) @@ -286,7 +285,7 @@ library TermAuctionList { // Handle new or unseen repo tokens /// @dev offer processed, but auctionClosed not yet called and auction is new so repoToken not on List and wont be picked up /// checking repoTokendiscountRates to make sure we are not double counting on re-openings - if (offer.termAuction.auctionCompleted() && repoTokenListData.discountRates[offer.repoToken] == 0) { + if (repoTokenListData.discountRates[offer.repoToken] == 0 && offer.termAuction.auctionCompleted()) { if (!offer.isRepoTokenSeen) { uint256 repoTokenAmountInBaseAssetPrecision = RepoTokenUtils.getNormalizedRepoTokenAmount( offer.repoToken, @@ -358,7 +357,7 @@ library TermAuctionList { // Handle new repo tokens or reopening auctions /// @dev offer processed, but auctionClosed not yet called and auction is new so repoToken not on List and wont be picked up /// checking repoTokendiscountRates to make sure we are not double counting on re-openings - if (offer.termAuction.auctionCompleted() && repoTokenListData.discountRates[offer.repoToken] == 0) { + if (repoTokenListData.discountRates[offer.repoToken] == 0 && offer.termAuction.auctionCompleted()) { // use normalized repoToken amount if repoToken is not in the list if (!offer.isRepoTokenSeen) { offerAmount = RepoTokenUtils.getNormalizedRepoTokenAmount( diff --git a/src/TermVaultEventEmitter.sol b/src/TermVaultEventEmitter.sol index 84240332..d5716c36 100644 --- a/src/TermVaultEventEmitter.sol +++ b/src/TermVaultEventEmitter.sol @@ -33,6 +33,7 @@ contract TermVaultEventEmitter is Initializable, UUPSUpgradeable, AccessControlU function pairVaultContract(address vaultContract) external onlyRole(ADMIN_ROLE){ _grantRole(VAULT_CONTRACT, vaultContract); + emit VaultContractPaired(vaultContract); } function emitTermControllerUpdated(address oldController, address newController) external onlyRole(VAULT_CONTRACT) { diff --git a/src/interfaces/term/ITermVaultEvents.sol b/src/interfaces/term/ITermVaultEvents.sol index 5ebb1076..c7ec9e79 100644 --- a/src/interfaces/term/ITermVaultEvents.sol +++ b/src/interfaces/term/ITermVaultEvents.sol @@ -2,6 +2,8 @@ pragma solidity ^0.8.18; interface ITermVaultEvents { + event VaultContractPaired(address vault); + event TermControllerUpdated(address oldController, address newController); event TimeToMaturityThresholdUpdated(uint256 oldThreshold, uint256 newThreshold); diff --git a/src/periphery/StrategyAprOracle.sol b/src/periphery/StrategyAprOracle.sol index 1c0bfb8f..96d0f8ab 100644 --- a/src/periphery/StrategyAprOracle.sol +++ b/src/periphery/StrategyAprOracle.sol @@ -28,7 +28,7 @@ contract StrategyAprOracle is AprOracleBase { function aprAfterDebtChange( address _strategy, int256 _delta - ) external view override returns (uint256) { + ) external pure override returns (uint256) { // TODO: Implement any necessary logic to return the most accurate // APR estimation for the strategy. return 1e17;