Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Repo redemption haircut #14

Closed
wants to merge 5 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
81 changes: 44 additions & 37 deletions src/RepoTokenList.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
24 changes: 16 additions & 8 deletions src/Strategy.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand All @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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(
Expand Down
19 changes: 17 additions & 2 deletions src/TermDiscountRateAdapter.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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_);
}

/**
Expand All @@ -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;
}
Comment on lines +52 to +54
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review: New function setRepoRedemptionHaircut.

The function setRepoRedemptionHaircut is correctly implemented with role-based access control using onlyRole(DEVOPS_ROLE). This ensures that only authorized users can modify the haircut values. However, consider adding input validation for the haircut parameter to ensure it falls within expected ranges, especially since it represents financial data.

Consider adding input validation for the haircut parameter:

require(haircut <= 100 * 10**18, "Haircut value is too high"); // Assuming 100% is the maximum valid haircut

}
1 change: 1 addition & 0 deletions src/interfaces/term/ITermDiscountRateAdapter.sol
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,6 @@
pragma solidity ^0.8.18;

interface ITermDiscountRateAdapter {
function repoRedemptionHaircut(address) external view returns (uint256);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Specify parameter name for clarity in repoRedemptionHaircut.

The function repoRedemptionHaircut lacks a parameter name, which could lead to confusion or errors when implementing the interface. It is recommended to specify a parameter name to improve readability and maintainability.

Consider modifying the function declaration as follows:

-function repoRedemptionHaircut(address) external view returns (uint256);
+function repoRedemptionHaircut(address repoAddress) external view returns (uint256);
Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
function repoRedemptionHaircut(address) external view returns (uint256);
function repoRedemptionHaircut(address repoAddress) external view returns (uint256);

function getDiscountRate(address repoToken) external view returns (uint256);
}
2 changes: 1 addition & 1 deletion src/test/utils/Setup.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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));
Expand Down
Loading