diff --git a/src/RepoTokenList.sol b/src/RepoTokenList.sol index 4339d388..1c586604 100644 --- a/src/RepoTokenList.sol +++ b/src/RepoTokenList.sol @@ -113,6 +113,7 @@ library RepoTokenList { /** * @notice This function calculates the cumulative weighted time to maturity and cumulative amount of all repoTokens in the list. * @param listData The list data + * @param discountRateAdapter The discount rate adapter * @param repoToken The address of the repoToken (optional) * @param repoTokenAmount The amount of the repoToken (optional) * @param purchaseTokenPrecision The precision of the purchase token @@ -127,6 +128,7 @@ library RepoTokenList { */ function getCumulativeRepoTokenData( RepoTokenListData storage listData, + ITermDiscountRateAdapter discountRateAdapter, address repoToken, uint256 repoTokenAmount, uint256 purchaseTokenPrecision @@ -148,12 +150,10 @@ library RepoTokenList { } // Convert the repo token balance to base asset precision - uint256 redemptionValue = ITermRepoToken(current).redemptionValue(); - uint256 repoTokenPrecision = 10**ERC20(current).decimals(); - uint256 repoTokenBalanceInBaseAssetPrecision = - (redemptionValue * repoTokenBalance * purchaseTokenPrecision) / - (repoTokenPrecision * RepoTokenUtils.RATE_PRECISION); + RepoTokenUtils.getNormalizedRepoTokenAmount( + current, repoTokenBalance, purchaseTokenPrecision, discountRateAdapter.repoRedemptionHaircut(current) + ); // Calculate the weighted time to maturity uint256 weightedTimeToMaturity = getRepoTokenWeightedTimeToMaturity( @@ -173,6 +173,7 @@ library RepoTokenList { /** * @notice Get the present value of repoTokens * @param listData The list data + * @param discountRateAdapter The discount rate adapter * @param purchaseTokenPrecision The precision of the purchase token * @param repoTokenToMatch The address of the repoToken to match (optional) * @return totalPresentValue The total present value of the repoTokens @@ -186,6 +187,7 @@ library RepoTokenList { */ function getPresentValue( RepoTokenListData storage listData, + ITermDiscountRateAdapter discountRateAdapter, uint256 purchaseTokenPrecision, address repoTokenToMatch ) internal view returns (uint256 totalPresentValue) { @@ -204,14 +206,14 @@ library RepoTokenList { uint256 currentMaturity = getRepoTokenMaturity(current); uint256 repoTokenBalance = ITermRepoToken(current).balanceOf(address(this)); - uint256 repoTokenPrecision = 10**ERC20(current).decimals(); - uint256 discountRate = listData.discountRates[current]; + uint256 discountRate = discountRateAdapter.getDiscountRate(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); + RepoTokenUtils.getNormalizedRepoTokenAmount( + current, repoTokenBalance, purchaseTokenPrecision, discountRateAdapter.repoRedemptionHaircut(current) + ); // Calculate present value based on maturity if (currentMaturity > block.timestamp) { diff --git a/src/RepoTokenUtils.sol b/src/RepoTokenUtils.sol index 6ed920d4..ee9dde0f 100644 --- a/src/RepoTokenUtils.sol +++ b/src/RepoTokenUtils.sol @@ -44,17 +44,19 @@ library RepoTokenUtils { * @param repoToken The address of the repoToken * @param repoTokenAmount The amount of the repoToken * @param purchaseTokenPrecision The precision of the purchase token + * @param repoRedemptionHaircut The haircut to be applied to the repoToken for bad debt * @return repoTokenAmountInBaseAssetPrecision The normalized amount of the repoToken in base asset precision */ function getNormalizedRepoTokenAmount( address repoToken, uint256 repoTokenAmount, - uint256 purchaseTokenPrecision + uint256 purchaseTokenPrecision, + uint256 repoRedemptionHaircut ) internal view returns (uint256 repoTokenAmountInBaseAssetPrecision) { uint256 repoTokenPrecision = 10**ERC20(repoToken).decimals(); uint256 redemptionValue = ITermRepoToken(repoToken).redemptionValue(); repoTokenAmountInBaseAssetPrecision = - (redemptionValue * repoTokenAmount * purchaseTokenPrecision) / - (repoTokenPrecision * RepoTokenUtils.RATE_PRECISION); + (redemptionValue * repoRedemptionHaircut * repoTokenAmount * purchaseTokenPrecision) / + (repoTokenPrecision * RATE_PRECISION * 1e18); } } \ No newline at end of file diff --git a/src/Strategy.sol b/src/Strategy.sol index dfc570c5..fd7ccf78 100644 --- a/src/Strategy.sol +++ b/src/Strategy.sol @@ -351,15 +351,14 @@ contract Strategy is BaseStrategy, Pausable, ReentrancyGuard { ITermRepoToken(repoToken), address(asset) ); - + uint256 discountRate = discountRateAdapter.getDiscountRate(repoToken); - uint256 repoTokenPrecision = 10 ** ERC20(repoToken).decimals(); - repoTokenAmountInBaseAssetPrecision = (ITermRepoToken( - repoToken - ).redemptionValue() * - amount * - PURCHASE_TOKEN_PRECISION) / - (repoTokenPrecision * RepoTokenUtils.RATE_PRECISION); + repoTokenAmountInBaseAssetPrecision = RepoTokenUtils.getNormalizedRepoTokenAmount( + repoToken, + amount, + PURCHASE_TOKEN_PRECISION, + discountRateAdapter.repoRedemptionHaircut(repoToken) + ); proceeds = RepoTokenUtils.calculatePresentValue( repoTokenAmountInBaseAssetPrecision, PURCHASE_TOKEN_PRECISION, @@ -398,15 +397,16 @@ contract Strategy is BaseStrategy, Pausable, ReentrancyGuard { address repoToken, uint256 discountRate, uint256 amount - ) external view returns (uint256) { + ) public view returns (uint256) { (uint256 redemptionTimestamp, , , ) = ITermRepoToken(repoToken) .config(); - uint256 repoTokenPrecision = 10 ** ERC20(repoToken).decimals(); - uint256 repoTokenAmountInBaseAssetPrecision = (ITermRepoToken(repoToken) - .redemptionValue() * - amount * - PURCHASE_TOKEN_PRECISION) / - (repoTokenPrecision * RepoTokenUtils.RATE_PRECISION); + uint256 repoTokenAmountInBaseAssetPrecision = RepoTokenUtils + .getNormalizedRepoTokenAmount( + repoToken, + amount, + PURCHASE_TOKEN_PRECISION, + discountRateAdapter.repoRedemptionHaircut(repoToken) + ); return RepoTokenUtils.calculatePresentValue( repoTokenAmountInBaseAssetPrecision, @@ -428,8 +428,9 @@ contract Strategy is BaseStrategy, Pausable, ReentrancyGuard { function getRepoTokenHoldingValue( address repoToken ) public view returns (uint256) { - return + return repoTokenListData.getPresentValue( + discountRateAdapter, PURCHASE_TOKEN_PRECISION, repoToken ) + @@ -489,6 +490,7 @@ contract Strategy is BaseStrategy, Pausable, ReentrancyGuard { return liquidBalance + repoTokenListData.getPresentValue( + discountRateAdapter, PURCHASE_TOKEN_PRECISION, address(0) ) + @@ -596,6 +598,7 @@ contract Strategy is BaseStrategy, Pausable, ReentrancyGuard { uint256 cumulativeRepoTokenAmount, bool foundInRepoTokenList ) = repoTokenListData.getCumulativeRepoTokenData( + discountRateAdapter, repoToken, repoTokenAmount, PURCHASE_TOKEN_PRECISION @@ -611,6 +614,7 @@ contract Strategy is BaseStrategy, Pausable, ReentrancyGuard { bool foundInOfferList ) = termAuctionListData.getCumulativeOfferData( repoTokenListData, + discountRateAdapter, repoToken, repoTokenAmount, PURCHASE_TOKEN_PRECISION @@ -625,11 +629,13 @@ contract Strategy is BaseStrategy, Pausable, ReentrancyGuard { !foundInOfferList && repoToken != address(0) ) { + uint256 repoRedemptionHaircut = discountRateAdapter.repoRedemptionHaircut(repoToken); uint256 repoTokenAmountInBaseAssetPrecision = RepoTokenUtils .getNormalizedRepoTokenAmount( repoToken, repoTokenAmount, - PURCHASE_TOKEN_PRECISION + PURCHASE_TOKEN_PRECISION, + repoRedemptionHaircut ); cumulativeAmount += repoTokenAmountInBaseAssetPrecision; @@ -990,7 +996,7 @@ contract Strategy is BaseStrategy, Pausable, ReentrancyGuard { } // Validate and insert the repoToken into the list, retrieve auction rate and redemption timestamp - (uint256 discountRate, uint256 redemptionTimestamp) = repoTokenListData + (, uint256 redemptionTimestamp) = repoTokenListData .validateAndInsertRepoToken( ITermRepoToken(repoToken), discountRateAdapter, @@ -1006,13 +1012,15 @@ contract Strategy is BaseStrategy, Pausable, ReentrancyGuard { uint256 totalAssetValue = _totalAssetValue(liquidBalance); require(totalAssetValue > 0); - // Calculate the repoToken amount in base asset precision - uint256 repoTokenPrecision = 10 ** ERC20(repoToken).decimals(); - uint256 repoTokenAmountInBaseAssetPrecision = (ITermRepoToken(repoToken) - .redemptionValue() * - repoTokenAmount * - PURCHASE_TOKEN_PRECISION) / - (repoTokenPrecision * RepoTokenUtils.RATE_PRECISION); + uint256 discountRate = discountRateAdapter.getDiscountRate(repoToken); + + // Calculate the repoToken amount in base asset precision + uint256 repoTokenAmountInBaseAssetPrecision = RepoTokenUtils.getNormalizedRepoTokenAmount( + repoToken, + repoTokenAmount, + PURCHASE_TOKEN_PRECISION, + discountRateAdapter.repoRedemptionHaircut(repoToken) + ); // Calculate the proceeds from selling the repoToken uint256 proceeds = RepoTokenUtils.calculatePresentValue( diff --git a/src/TermAuctionList.sol b/src/TermAuctionList.sol index 17564a41..7a4554b8 100644 --- a/src/TermAuctionList.sol +++ b/src/TermAuctionList.sol @@ -290,7 +290,8 @@ library TermAuctionList { uint256 repoTokenAmountInBaseAssetPrecision = RepoTokenUtils.getNormalizedRepoTokenAmount( offer.repoToken, ITermRepoToken(offer.repoToken).balanceOf(address(this)), - purchaseTokenPrecision + purchaseTokenPrecision, + discountRateAdapter.repoRedemptionHaircut(offer.repoToken) ); totalValue += RepoTokenUtils.calculatePresentValue( repoTokenAmountInBaseAssetPrecision, @@ -315,6 +316,7 @@ library TermAuctionList { * @notice Get cumulative offer data for a specified repoToken * @param listData The list data * @param repoTokenListData The repoToken list data + * @param discountRateAdapter The discount rate adapter * @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 @@ -330,6 +332,7 @@ library TermAuctionList { function getCumulativeOfferData( TermAuctionListData storage listData, RepoTokenListData storage repoTokenListData, + ITermDiscountRateAdapter discountRateAdapter, address repoToken, uint256 newOfferAmount, uint256 purchaseTokenPrecision @@ -360,7 +363,8 @@ library TermAuctionList { offerAmount = RepoTokenUtils.getNormalizedRepoTokenAmount( offer.repoToken, ITermRepoToken(offer.repoToken).balanceOf(address(this)), - purchaseTokenPrecision + purchaseTokenPrecision, + discountRateAdapter.repoRedemptionHaircut(offer.repoToken) ); _markRepoTokenAsSeen(offers, offer.repoToken); diff --git a/src/TermDiscountRateAdapter.sol b/src/TermDiscountRateAdapter.sol index e6610ee7..ac8f5a48 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 ORACLE_ROLE = keccak256("ORACLE_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 oracleWallet_ The address of the oracle wallet */ - constructor(address termController_) { + constructor(address termController_, address oracleWallet_) { TERM_CONTROLLER = ITermController(termController_); + _grantRole(ORACLE_ROLE, oracleWallet_); } /** @@ -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(ORACLE_ROLE) { + repoRedemptionHaircut[repoToken] = haircut; + } +} \ No newline at end of file diff --git a/src/interfaces/term/ITermDiscountRateAdapter.sol b/src/interfaces/term/ITermDiscountRateAdapter.sol index 97fe4019..feba49c3 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 a2dc0fe4..fea7c67a 100644 --- a/src/test/utils/Setup.sol +++ b/src/test/utils/Setup.sol @@ -50,6 +50,7 @@ contract Setup is ExtendedTest, IEvents { address public performanceFeeRecipient = address(3); address public adminWallet = address(111); address public devopsWallet = address(222); + address public oracleWallet = address(333); // Address of the real deployed Factory address public factory; @@ -95,7 +96,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), oracleWallet); termVaultEventEmitterImpl = new TermVaultEventEmitter(); termVaultEventEmitter = TermVaultEventEmitter(address(new ERC1967Proxy(address(termVaultEventEmitterImpl), ""))); mockYearnVault = new ERC4626Mock(address(asset));