Skip to content

Commit

Permalink
Merge pull request #85 from term-finance/runtime-fv
Browse files Browse the repository at this point in the history
Runtime fv
  • Loading branch information
aazhou1 authored Nov 7, 2024
2 parents a537936 + 341b395 commit 6a42b85
Show file tree
Hide file tree
Showing 6 changed files with 150 additions and 45 deletions.
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());
}
}

0 comments on commit 6a42b85

Please sign in to comment.