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

Runtime fv #85

Merged
merged 9 commits into from
Nov 7, 2024
28 changes: 22 additions & 6 deletions src/RepoTokenList.sol
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// SPDX-License-Identifier: AGPL-3.0
pragma solidity ^0.8.18;

import {ITermController} from "./interfaces/term/ITermController.sol";
import {ITermRepoToken} from "./interfaces/term/ITermRepoToken.sol";
import {ITermRepoServicer} from "./interfaces/term/ITermRepoServicer.sol";
import {ITermRepoCollateralManager} from "./interfaces/term/ITermRepoCollateralManager.sol";
Expand All @@ -27,6 +28,7 @@ struct RepoTokenListData {
library RepoTokenList {
address public constant NULL_NODE = address(0);
uint256 internal constant INVALID_AUCTION_RATE = 0;
uint256 internal constant ZERO_AUCTION_RATE = 1; //Set to lowest nonzero number so that it is not confused with INVALID_AUCTION_RATe but still calculates as if 0.

error InvalidRepoToken(address token);

Expand Down Expand Up @@ -175,22 +177,32 @@ library RepoTokenList {
* @param listData The list data
* @param discountRateAdapter The discount rate adapter
* @param purchaseTokenPrecision The precision of the purchase token
* @param prevTermController The previous term controller
* @param currTermController The current term controller
* @return totalPresentValue The total present value of the repoTokens
* @dev Aggregates the present value of all repoTokens in the list.
*/
function getPresentValue(
RepoTokenListData storage listData,
ITermDiscountRateAdapter discountRateAdapter,
uint256 purchaseTokenPrecision
uint256 purchaseTokenPrecision,
ITermController prevTermController,
ITermController currTermController
) internal view returns (uint256 totalPresentValue) {
// If the list is empty, return 0
if (listData.head == NULL_NODE) return 0;

address current = listData.head;
address tokenTermController;
while (current != NULL_NODE) {
uint256 currentMaturity = getRepoTokenMaturity(current);
uint256 repoTokenBalance = ITermRepoToken(current).balanceOf(address(this));
uint256 discountRate = discountRateAdapter.getDiscountRate(current);
if (currTermController.isTermDeployed(current)){
tokenTermController = address(currTermController);
} else if (prevTermController.isTermDeployed(current)){
tokenTermController = address(prevTermController);
}
uint256 discountRate = discountRateAdapter.getDiscountRate(tokenTermController, current);

// Convert repo token balance to base asset precision
// (ratePrecision * repoPrecision * purchasePrecision) / (repoPrecision * ratePrecision) = purchasePrecision
Expand Down Expand Up @@ -356,16 +368,20 @@ library RepoTokenList {
return (false, discountRate, redemptionTimestamp); //revert InvalidRepoToken(address(repoToken));
}

uint256 oracleRate = discountRateAdapter.getDiscountRate(address(repoToken));
if (oracleRate != INVALID_AUCTION_RATE) {
uint256 oracleRate;
try discountRateAdapter.getDiscountRate(address(repoToken)) returns (uint256 rate) {
oracleRate = rate;
} catch {
}

if (oracleRate != 0) {
if (discountRate != oracleRate) {
listData.discountRates[address(repoToken)] = oracleRate;
}
}
} else {
try discountRateAdapter.getDiscountRate(address(repoToken)) returns (uint256 rate) {
discountRate = rate;

discountRate = rate == 0 ? ZERO_AUCTION_RATE : rate;
} catch {
discountRate = INVALID_AUCTION_RATE;
return (false, discountRate, redemptionTimestamp);
Expand Down
38 changes: 29 additions & 9 deletions src/Strategy.sol
Original file line number Diff line number Diff line change
Expand Up @@ -147,20 +147,28 @@ contract Strategy is BaseStrategy, Pausable, AccessControl, ReentrancyGuard {

/**
* @notice Set the term controller
* @param newTermController The address of the new term controller
* @param newTermControllerAddr The address of the new term controller
*/
function setTermController(
address newTermController
address newTermControllerAddr
) external onlyRole(GOVERNOR_ROLE) {
require(newTermController != address(0));
require(ITermController(newTermController).getProtocolReserveAddress() != address(0));
require(newTermControllerAddr != address(0));
require(ITermController(newTermControllerAddr).getProtocolReserveAddress() != address(0));
ITermController newTermController = ITermController(newTermControllerAddr);
address currentIteration = repoTokenListData.head;
while (currentIteration != address(0)) {
if (!currTermController.isTermDeployed(currentIteration) && !newTermController.isTermDeployed(currentIteration)) {
revert("repoToken not in controllers");
}
currentIteration = repoTokenListData.nodes[currentIteration].next;
}
address current = address(currTermController);
TERM_VAULT_EVENT_EMITTER.emitTermControllerUpdated(
current,
newTermController
newTermControllerAddr
);
prevTermController = ITermController(current);
currTermController = ITermController(newTermController);
currTermController = newTermController;
}

/**
Expand All @@ -171,7 +179,7 @@ contract Strategy is BaseStrategy, Pausable, AccessControl, ReentrancyGuard {
address newAdapter
) external onlyRole(GOVERNOR_ROLE) {
ITermDiscountRateAdapter newDiscountRateAdapter = ITermDiscountRateAdapter(newAdapter);
require(address(newDiscountRateAdapter.TERM_CONTROLLER()) != address(0));
require(address(newDiscountRateAdapter.currTermController()) != address(0));
TERM_VAULT_EVENT_EMITTER.emitDiscountRateAdapterUpdated(
address(discountRateAdapter),
newAdapter
Expand Down Expand Up @@ -477,9 +485,15 @@ contract Strategy is BaseStrategy, Pausable, AccessControl, ReentrancyGuard {
) public view returns (uint256) {
uint256 repoTokenHoldingPV;
if (repoTokenListData.discountRates[repoToken] != 0) {
address tokenTermController;
if (currTermController.isTermDeployed(repoToken)){
tokenTermController = address(currTermController);
} else if (prevTermController.isTermDeployed(repoToken)){
tokenTermController = address(prevTermController);
}
repoTokenHoldingPV = calculateRepoTokenPresentValue(
repoToken,
discountRateAdapter.getDiscountRate(repoToken),
discountRateAdapter.getDiscountRate(tokenTermController, repoToken),
ITermRepoToken(repoToken).balanceOf(address(this))
);
}
Expand All @@ -489,6 +503,8 @@ contract Strategy is BaseStrategy, Pausable, AccessControl, ReentrancyGuard {
repoTokenListData,
discountRateAdapter,
PURCHASE_TOKEN_PRECISION,
prevTermController,
currTermController,
repoToken
);
}
Expand Down Expand Up @@ -542,12 +558,16 @@ contract Strategy is BaseStrategy, Pausable, AccessControl, ReentrancyGuard {
liquidBalance +
repoTokenListData.getPresentValue(
discountRateAdapter,
PURCHASE_TOKEN_PRECISION
PURCHASE_TOKEN_PRECISION,
prevTermController,
currTermController
) +
termAuctionListData.getPresentValue(
repoTokenListData,
discountRateAdapter,
PURCHASE_TOKEN_PRECISION,
prevTermController,
currTermController,
address(0)
);
}
Expand Down
11 changes: 11 additions & 0 deletions src/TermAuctionList.sol
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// SPDX-License-Identifier: AGPL-3.0
pragma solidity ^0.8.18;

import {ITermController} from "./interfaces/term/ITermController.sol";
import {ITermAuction} from "./interfaces/term/ITermAuction.sol";
import {ITermAuctionOfferLocker} from "./interfaces/term/ITermAuctionOfferLocker.sol";
import {ITermRepoToken} from "./interfaces/term/ITermRepoToken.sol";
Expand Down Expand Up @@ -241,6 +242,8 @@ library TermAuctionList {
* @param repoTokenListData The repoToken list data
* @param discountRateAdapter The discount rate adapter
* @param purchaseTokenPrecision The precision of the purchase token
* @param prevTermController The previous term controller
* @param currTermController The current term controller
* @param repoTokenToMatch The address of the repoToken to match (optional)
* @return totalValue The total present value of the offers
*
Expand All @@ -254,6 +257,8 @@ library TermAuctionList {
RepoTokenListData storage repoTokenListData,
ITermDiscountRateAdapter discountRateAdapter,
uint256 purchaseTokenPrecision,
ITermController prevTermController,
ITermController currTermController,
address repoTokenToMatch
) internal view returns (uint256 totalValue) {
// Return 0 if the list is empty
Expand All @@ -273,6 +278,7 @@ library TermAuctionList {
}

uint256 offerAmount = offer.offerLocker.lockedOffer(current).amount;
address tokenTermController;

// 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
Expand All @@ -285,6 +291,11 @@ library TermAuctionList {
purchaseTokenPrecision,
discountRateAdapter.repoRedemptionHaircut(offer.repoToken)
);
if (currTermController.isTermDeployed(offer.repoToken)){
tokenTermController = address(currTermController);
} else if (prevTermController.isTermDeployed(offer.repoToken)){
tokenTermController = address(prevTermController);
}
totalValue += RepoTokenUtils.calculatePresentValue(
repoTokenAmountInBaseAssetPrecision,
purchaseTokenPrecision,
Expand Down
107 changes: 80 additions & 27 deletions src/TermDiscountRateAdapter.sol
Original file line number Diff line number Diff line change
Expand Up @@ -9,50 +9,52 @@ import {AccessControl} from "@openzeppelin/contracts/access/AccessControl.sol";
contract TermDiscountRateAdapter is ITermDiscountRateAdapter, AccessControl {
bytes32 public constant ORACLE_ROLE = keccak256("ORACLE_ROLE");

ITermController public immutable TERM_CONTROLLER;
/// @dev Previous term controller
ITermController public prevTermController;
/// @dev Current term controller
ITermController public currTermController;
mapping(address => mapping (bytes32 => bool)) public rateInvalid;
mapping(address => uint256) public repoRedemptionHaircut;

constructor(address termController_, address oracleWallet_) {
TERM_CONTROLLER = ITermController(termController_);
currTermController = ITermController(termController_);
_grantRole(ORACLE_ROLE, oracleWallet_);
}

/**
* @notice Retrieves the discount rate for a given repo token
* @param termController The address of the term controller
* @param repoToken The address of the repo token
* @return The discount rate for the specified repo token
* @dev This function fetches the auction results for the repo token's term repo ID
* and returns the clearing rate of the most recent auction
*/
function getDiscountRate(address repoToken) public view virtual returns (uint256) {
function getDiscountRate(address termController, address repoToken) public view virtual returns (uint256) {

if (repoToken == address(0)) return 0;
(AuctionMetadata[] memory auctionMetadata, ) = TERM_CONTROLLER.getTermAuctionResults(ITermRepoToken(repoToken).termRepoId());

uint256 len = auctionMetadata.length;
require(len > 0, "No auctions found");

// If there is a re-opening auction, e.g. 2 or more results for the same token
if (len > 1) {
uint256 latestAuctionTime = auctionMetadata[len - 1].auctionClearingBlockTimestamp;
if ((block.timestamp - latestAuctionTime) < 30 minutes) {
for (int256 i = int256(len) - 2; i >= 0; i--) {
if (!rateInvalid[repoToken][auctionMetadata[uint256(i)].termAuctionId]) {
return auctionMetadata[uint256(i)].auctionClearingRate;
}
}
} else {
for (int256 i = int256(len) - 1; i >= 0; i--) {
if (!rateInvalid[repoToken][auctionMetadata[uint256(i)].termAuctionId]) {
return auctionMetadata[uint256(i)].auctionClearingRate;
}
}
}
revert("No valid auction rate found");
ITermController tokenTermController;
if (termController == address(prevTermController)) {
tokenTermController = prevTermController;
} else if (termController == address(currTermController)) {
tokenTermController = currTermController;
} else {
revert("Invalid term controller");
}
return _getDiscountRate(tokenTermController, repoToken);
}

// If there is only 1 result (not a re-opening) then always return result
return auctionMetadata[0].auctionClearingRate;
/**
* @notice Retrieves the discount rate for a given repo token
* @param repoToken The address of the repo token
* @return The discount rate for the specified repo token
* @dev This function fetches the auction results for the repo token's term repo ID
* and returns the clearing rate of the most recent auction
*/
function getDiscountRate(address repoToken) public view virtual returns (uint256) {
if (repoToken == address(0)) return 0;
ITermController tokenTermController = _identifyTermController(repoToken);
return _getDiscountRate(tokenTermController, repoToken);
}

/**
Expand All @@ -68,19 +70,30 @@ contract TermDiscountRateAdapter is ITermDiscountRateAdapter, AccessControl {
bytes32 termAuctionId,
bool isInvalid
) external onlyRole(ORACLE_ROLE) {
ITermController tokenTermController = _identifyTermController(repoToken);
// Fetch the auction metadata for the given repo token
(AuctionMetadata[] memory auctionMetadata, ) = TERM_CONTROLLER.getTermAuctionResults(ITermRepoToken(repoToken).termRepoId());
(AuctionMetadata[] memory auctionMetadata, ) = tokenTermController.getTermAuctionResults(ITermRepoToken(repoToken).termRepoId());

// Check if the termAuctionId exists in the metadata
bool auctionExists = _validateAuctionExistence(auctionMetadata, termAuctionId);

require(auctionMetadata.length > 1, "Cannot invalidate the only auction result");
// Revert if the auction doesn't exist
require(auctionExists, "Auction ID not found in metadata");

// Update the rate invalidation status
rateInvalid[repoToken][termAuctionId] = isInvalid;
}

/**
* @notice Sets the term controller
* @param termController The address of the term controller
*/
function setTermController(address termController) external onlyRole(ORACLE_ROLE) {
prevTermController = currTermController;
currTermController = ITermController(termController);
}

/**
* @notice Set the repo redemption haircut
* @param repoToken The address of the repo token
Expand All @@ -90,6 +103,46 @@ contract TermDiscountRateAdapter is ITermDiscountRateAdapter, AccessControl {
repoRedemptionHaircut[repoToken] = haircut;
}

function _identifyTermController(address termRepoToken) internal view returns (ITermController) {
if (currTermController.isTermDeployed(termRepoToken)) {
return currTermController;
} else if (prevTermController.isTermDeployed(termRepoToken)) {
return prevTermController;
} else {
revert("Term controller not found");
}
}

function _getDiscountRate(ITermController termController, address repoToken) internal view returns (uint256) {
(AuctionMetadata[] memory auctionMetadata, ) = termController.getTermAuctionResults(ITermRepoToken(repoToken).termRepoId());

uint256 len = auctionMetadata.length;
require(len > 0, "No auctions found");

// If there is a re-opening auction, e.g. 2 or more results for the same token
if (len > 1) {
uint256 latestAuctionTime = auctionMetadata[len - 1].auctionClearingBlockTimestamp;
if ((block.timestamp - latestAuctionTime) < 30 minutes) {
for (int256 i = int256(len) - 2; i >= 0; i--) {
if (!rateInvalid[repoToken][auctionMetadata[uint256(i)].termAuctionId]) {
return auctionMetadata[uint256(i)].auctionClearingRate;
}
}
} else {
for (int256 i = int256(len) - 1; i >= 0; i--) {
if (!rateInvalid[repoToken][auctionMetadata[uint256(i)].termAuctionId]) {
return auctionMetadata[uint256(i)].auctionClearingRate;
}
}
}
revert("No valid auction rate found");
}

// If there is only 1 result (not a re-opening) then always return result
return auctionMetadata[0].auctionClearingRate;
}


function _validateAuctionExistence(AuctionMetadata[] memory auctionMetadata, bytes32 termAuctionId) private view returns(bool auctionExists) {
// Check if the termAuctionId exists in the metadata
bool auctionExists;
Expand Down
3 changes: 2 additions & 1 deletion src/interfaces/term/ITermDiscountRateAdapter.sol
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@ pragma solidity ^0.8.18;

import {ITermController} from "./ITermController.sol";
interface ITermDiscountRateAdapter {
function TERM_CONTROLLER() external view returns (ITermController);
function currTermController() external view returns (ITermController);
function repoRedemptionHaircut(address) external view returns (uint256);
function getDiscountRate(address repoToken) external view returns (uint256);
function getDiscountRate(address termController, address repoToken) external view returns (uint256);
}
8 changes: 6 additions & 2 deletions src/test/kontrol/TermDiscountRateAdapter.sol
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,15 @@ contract TermDiscountRateAdapter is ITermDiscountRateAdapter, KontrolTest {
return _repoRedemptionHaircut[repoToken];
}

function getDiscountRate(address repoToken) external view returns (uint256) {
function getDiscountRate(address termController, address repoToken) external view returns (uint256) {
return _discountRate[repoToken];
}

function TERM_CONTROLLER() external view returns (ITermController) {
function getDiscountRate( address repoToken) external view returns (uint256) {
return _discountRate[repoToken];
}

function currTermController() external view returns (ITermController) {
return ITermController(kevm.freshAddress());
}
}