diff --git a/src/RepoTokenList.sol b/src/RepoTokenList.sol index 1767a32..763400f 100644 --- a/src/RepoTokenList.sol +++ b/src/RepoTokenList.sol @@ -187,56 +187,63 @@ library RepoTokenList { function getPresentValue( RepoTokenListData storage listData, uint256 purchaseTokenPrecision, - address repoTokenToMatch + address repoTokenToMatch, + uint256 repoRedemptionHaircutMantissa ) internal view returns (uint256 totalPresentValue) { // If the list is empty, return 0 if (listData.head == NULL_NODE) return 0; - address current = listData.head; - while (current != NULL_NODE) { - // Filter by a specific repoToken, address(0) bypasses this filter - if (repoTokenToMatch != address(0) && current != repoTokenToMatch) { - // Not a match, do not add to totalPresentValue + if (repoTokenToMatch != address(0) && listData.discountRates[repoTokenToMatch] != INVALID_AUCTION_RATE) { + totalPresentValue = _getTotalPresentValue(listData, purchaseTokenPrecision, repoTokenToMatch, repoRedemptionHaircutMantissa); + } else { + address current = listData.head; + while (current != NULL_NODE) { + totalPresentValue += _getTotalPresentValue(listData, purchaseTokenPrecision, current, repoRedemptionHaircutMantissa ); + // Move to the next token in the list current = _getNext(listData, current); - continue; - } - - uint256 currentMaturity = getRepoTokenMaturity(current); - uint256 repoTokenBalance = ITermRepoToken(current).balanceOf(address(this)); - uint256 repoTokenPrecision = 10**ERC20(current).decimals(); - uint256 discountRate = listData.discountRates[current]; - - // Convert repo token balance to base asset precision - // (ratePrecision * repoPrecision * purchasePrecision) / (repoPrecision * ratePrecision) = purchasePrecision - uint256 repoTokenBalanceInBaseAssetPrecision = - (ITermRepoToken(current).redemptionValue() * repoTokenBalance * purchaseTokenPrecision) / - (repoTokenPrecision * RepoTokenUtils.RATE_PRECISION); - - // Calculate present value based on maturity - if (currentMaturity > block.timestamp) { - totalPresentValue += RepoTokenUtils.calculatePresentValue( - repoTokenBalanceInBaseAssetPrecision, purchaseTokenPrecision, currentMaturity, discountRate - ); - } else { - totalPresentValue += repoTokenBalanceInBaseAssetPrecision; - } - - // Filter by a specific repo token, address(0) bypasses this condition - if (repoTokenToMatch != address(0) && current == repoTokenToMatch) { - // Found a match, terminate early - break; } - - // Move to the next token in the list - current = _getNext(listData, current); - } + } } /*////////////////////////////////////////////////////////////// INTERNAL FUNCTIONS //////////////////////////////////////////////////////////////*/ + /** + * @notice Calculates the total present value of an asset based on its future value and discount rate. + * @param listData The list data + * @param purchaseTokenPrecision The precision of the purchase token + * @param repoTokenToMatch The address of the repoToken to match + * @return The total present value of the asset. + */ + function _getTotalPresentValue( + RepoTokenListData storage listData, + uint256 purchaseTokenPrecision, + address repoTokenToMatch, + uint256 repoRedemptionHaircutMantissa + + ) private view returns (uint256) { + uint256 currentMaturity = getRepoTokenMaturity(repoTokenToMatch); + uint256 repoTokenBalance = ITermRepoToken(repoTokenToMatch).balanceOf(address(this)); + uint256 repoTokenPrecision = 10 ** ERC20(repoTokenToMatch).decimals(); + uint256 discountRate = listData.discountRates[repoTokenToMatch]; + + // Convert repo token balance to base asset precision + // (ratePrecision * repoPrecision * purchasePrecision) / (repoPrecision * ratePrecision) = purchasePrecision + uint256 repoTokenBalanceInBaseAssetPrecision = (ITermRepoToken(repoTokenToMatch).redemptionValue() * repoRedemptionHaircutMantissa * + repoTokenBalance * + purchaseTokenPrecision) / (repoTokenPrecision * RepoTokenUtils.RATE_PRECISION * 1e18); + + // Calculate present value based on maturity + if (currentMaturity > block.timestamp) { + return + RepoTokenUtils.calculatePresentValue(repoTokenBalanceInBaseAssetPrecision, purchaseTokenPrecision, currentMaturity, discountRate); + } else { + return repoTokenBalanceInBaseAssetPrecision; + } +} + /** * @notice Calculates the time remaining until a repoToken matures * @param redemptionTimestamp The redemption timestamp of the repoToken diff --git a/src/Strategy.sol b/src/Strategy.sol index cb3a486..a085b77 100644 --- a/src/Strategy.sol +++ b/src/Strategy.sol @@ -65,6 +65,7 @@ contract Strategy is BaseStrategy, Pausable, ReentrancyGuard { uint256 public discountRateMarkup; // 1e18 (TODO: check this) uint256 public repoTokenConcentrationLimit; // 1e18 mapping(address => bool) public repoTokenBlacklist; + bool public depositLock; modifier notBlacklisted(address repoToken) { @@ -353,13 +354,14 @@ contract Strategy is BaseStrategy, Pausable, ReentrancyGuard { ); uint256 discountRate = discountRateAdapter.getDiscountRate(repoToken); + uint256 repoRedemptionHaircutMantissa = discountRateAdapter.repoRedemptionHaircut(repoToken) == 0 ? 1e18 : discountRateAdapter.repoRedemptionHaircut(repoToken); uint256 repoTokenPrecision = 10 ** ERC20(repoToken).decimals(); repoTokenAmountInBaseAssetPrecision = (ITermRepoToken( repoToken - ).redemptionValue() * + ).redemptionValue() * repoRedemptionHaircutMantissa * amount * PURCHASE_TOKEN_PRECISION) / - (repoTokenPrecision * RepoTokenUtils.RATE_PRECISION); + (repoTokenPrecision * RepoTokenUtils.RATE_PRECISION * 1e18); proceeds = RepoTokenUtils.calculatePresentValue( repoTokenAmountInBaseAssetPrecision, PURCHASE_TOKEN_PRECISION, @@ -402,11 +404,12 @@ contract Strategy is BaseStrategy, Pausable, ReentrancyGuard { (uint256 redemptionTimestamp, , , ) = ITermRepoToken(repoToken) .config(); uint256 repoTokenPrecision = 10 ** ERC20(repoToken).decimals(); + uint256 repoRedemptionHaircutMantissa = discountRateAdapter.repoRedemptionHaircut(repoToken) == 0 ? 1e18 : discountRateAdapter.repoRedemptionHaircut(repoToken); uint256 repoTokenAmountInBaseAssetPrecision = (ITermRepoToken(repoToken) - .redemptionValue() * + .redemptionValue() * repoRedemptionHaircutMantissa * amount * PURCHASE_TOKEN_PRECISION) / - (repoTokenPrecision * RepoTokenUtils.RATE_PRECISION); + (repoTokenPrecision * RepoTokenUtils.RATE_PRECISION * 1e18); return RepoTokenUtils.calculatePresentValue( repoTokenAmountInBaseAssetPrecision, @@ -428,10 +431,12 @@ contract Strategy is BaseStrategy, Pausable, ReentrancyGuard { function getRepoTokenHoldingValue( address repoToken ) public view returns (uint256) { + uint256 repoRedemptionHaircutMantissa = discountRateAdapter.repoRedemptionHaircut(address(asset)) == 0 ? 1e18 : discountRateAdapter.repoRedemptionHaircut(address(asset)); return repoTokenListData.getPresentValue( PURCHASE_TOKEN_PRECISION, - repoToken + repoToken, + repoRedemptionHaircutMantissa ) + termAuctionListData.getPresentValue( repoTokenListData, @@ -486,11 +491,13 @@ contract Strategy is BaseStrategy, Pausable, ReentrancyGuard { * and the present value of all pending offers to calculate the total asset value. */ function _totalAssetValue(uint256 liquidBalance) internal view returns (uint256 totalValue) { + uint256 repoRedemptionHaircutMantissa = discountRateAdapter.repoRedemptionHaircut(address(asset)) == 0 ? 1e18 : discountRateAdapter.repoRedemptionHaircut(address(asset)); return liquidBalance + repoTokenListData.getPresentValue( PURCHASE_TOKEN_PRECISION, - address(0) + address(0), + repoRedemptionHaircutMantissa ) + termAuctionListData.getPresentValue( repoTokenListData, @@ -1026,11 +1033,12 @@ contract Strategy is BaseStrategy, Pausable, ReentrancyGuard { // Calculate the repoToken amount in base asset precision uint256 repoTokenPrecision = 10 ** ERC20(repoToken).decimals(); + uint256 repoRedemptionHaircutMantissa = discountRateAdapter.repoRedemptionHaircut(repoToken) == 0 ? 1e18 : discountRateAdapter.repoRedemptionHaircut(repoToken); uint256 repoTokenAmountInBaseAssetPrecision = (ITermRepoToken(repoToken) - .redemptionValue() * + .redemptionValue() * repoRedemptionHaircutMantissa * repoTokenAmount * PURCHASE_TOKEN_PRECISION) / - (repoTokenPrecision * RepoTokenUtils.RATE_PRECISION); + (repoTokenPrecision * RepoTokenUtils.RATE_PRECISION * 1e18); // Calculate the proceeds from selling the repoToken uint256 proceeds = RepoTokenUtils.calculatePresentValue( diff --git a/src/TermDiscountRateAdapter.sol b/src/TermDiscountRateAdapter.sol index e6610ee..1c8bed4 100644 --- a/src/TermDiscountRateAdapter.sol +++ b/src/TermDiscountRateAdapter.sol @@ -4,22 +4,28 @@ pragma solidity ^0.8.18; import {ITermDiscountRateAdapter} from "./interfaces/term/ITermDiscountRateAdapter.sol"; import {ITermController, AuctionMetadata} from "./interfaces/term/ITermController.sol"; import {ITermRepoToken} from "./interfaces/term/ITermRepoToken.sol"; +import "@openzeppelin/contracts-upgradeable/contracts/access/AccessControlUpgradeable.sol"; /** * @title TermDiscountRateAdapter * @notice Adapter contract to retrieve discount rates for Term repo tokens * @dev This contract implements the ITermDiscountRateAdapter interface and interacts with the Term Controller */ -contract TermDiscountRateAdapter is ITermDiscountRateAdapter { +contract TermDiscountRateAdapter is ITermDiscountRateAdapter, AccessControlUpgradeable { + + bytes32 public constant DEVOPS_ROLE = keccak256("DEVOPS_ROLE"); /// @notice The Term Controller contract ITermController public immutable TERM_CONTROLLER; + mapping(address => uint256) public repoRedemptionHaircut; /** * @notice Constructor to initialize the TermDiscountRateAdapter * @param termController_ The address of the Term Controller contract + * @param devopsWallet_ The address of the devops wallet */ - constructor(address termController_) { + constructor(address termController_, address devopsWallet_) { TERM_CONTROLLER = ITermController(termController_); + _grantRole(DEVOPS_ROLE, devopsWallet_); } /** @@ -37,4 +43,13 @@ contract TermDiscountRateAdapter is ITermDiscountRateAdapter { return auctionMetadata[len - 1].auctionClearingRate; } + + /** + * @notice Set the repo redemption haircut + * @param repoToken The address of the repo token + * @param haircut The repo redemption haircut in 18 decimals + */ + function setRepoRedemptionHaircut(address repoToken, uint256 haircut) external onlyRole(DEVOPS_ROLE) { + repoRedemptionHaircut[repoToken] = haircut; + } } diff --git a/src/interfaces/term/ITermDiscountRateAdapter.sol b/src/interfaces/term/ITermDiscountRateAdapter.sol index 97fe401..feba49c 100644 --- a/src/interfaces/term/ITermDiscountRateAdapter.sol +++ b/src/interfaces/term/ITermDiscountRateAdapter.sol @@ -2,5 +2,6 @@ pragma solidity ^0.8.18; interface ITermDiscountRateAdapter { + function repoRedemptionHaircut(address) external view returns (uint256); function getDiscountRate(address repoToken) external view returns (uint256); } diff --git a/src/test/utils/Setup.sol b/src/test/utils/Setup.sol index a2dc0fe..56187ea 100644 --- a/src/test/utils/Setup.sol +++ b/src/test/utils/Setup.sol @@ -95,7 +95,7 @@ contract Setup is ExtendedTest, IEvents { vm.etch(0xBB51273D6c746910C7C06fe718f30c936170feD0, address(tokenizedStrategy).code); termController = new MockTermController(); - discountRateAdapter = new TermDiscountRateAdapter(address(termController)); + discountRateAdapter = new TermDiscountRateAdapter(address(termController), devopsWallet); termVaultEventEmitterImpl = new TermVaultEventEmitter(); termVaultEventEmitter = TermVaultEventEmitter(address(new ERC1967Proxy(address(termVaultEventEmitterImpl), ""))); mockYearnVault = new ERC4626Mock(address(asset));