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

Adding repo concentration limit #5

Merged
merged 9 commits into from
Aug 15, 2024
Merged
Show file tree
Hide file tree
Changes from 6 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
30 changes: 13 additions & 17 deletions src/RepoTokenList.sol
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import "forge-std/console.sol";
import {ITermRepoToken} from "./interfaces/term/ITermRepoToken.sol";
import {ITermRepoServicer} from "./interfaces/term/ITermRepoServicer.sol";
import {ITermRepoCollateralManager} from "./interfaces/term/ITermRepoCollateralManager.sol";
import {ITermDiscountRateAdapter} from "./interfaces/term/ITermDiscountRateAdapter.sol";
import {ITermController, AuctionMetadata} from "./interfaces/term/ITermController.sol";
import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import {RepoTokenUtils} from "./RepoTokenUtils.sol";
Expand Down Expand Up @@ -158,18 +159,6 @@ library RepoTokenList {
}
}

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

uint256 len = auctionMetadata.length;

if (len == 0) {
revert InvalidRepoToken(address(repoToken));
}

return auctionMetadata[len - 1].auctionClearingRate;
}

function validateRepoToken(
RepoTokenListData storage listData,
ITermRepoToken repoToken,
Expand Down Expand Up @@ -212,9 +201,9 @@ library RepoTokenList {
RepoTokenListData storage listData,
ITermRepoToken repoToken,
ITermController termController,
ITermDiscountRateAdapter discountRateAdapter,
address asset
) internal returns (uint256 auctionRate, uint256 redemptionTimestamp)
{
) internal returns (uint256 auctionRate, uint256 redemptionTimestamp) {
auctionRate = listData.auctionRates[address(repoToken)];
if (auctionRate != INVALID_AUCTION_RATE) {
(redemptionTimestamp, , ,) = repoToken.config();
Expand All @@ -224,14 +213,14 @@ library RepoTokenList {
revert InvalidRepoToken(address(repoToken));
}

uint256 oracleRate = getAuctionRate(termController, repoToken);
uint256 oracleRate = discountRateAdapter.getDiscountRate(address(repoToken));
if (oracleRate != INVALID_AUCTION_RATE) {
if (auctionRate != oracleRate) {
listData.auctionRates[address(repoToken)] = oracleRate;
}
}
} else {
auctionRate = getAuctionRate(termController, repoToken);
auctionRate = discountRateAdapter.getDiscountRate(address(repoToken));

redemptionTimestamp = validateRepoToken(listData, repoToken, termController, asset);

Expand Down Expand Up @@ -283,7 +272,8 @@ library RepoTokenList {

function getPresentValue(
RepoTokenListData storage listData,
uint256 purchaseTokenPrecision
uint256 purchaseTokenPrecision,
address repoTokenToMatch
) internal view returns (uint256 totalPresentValue) {
if (listData.head == NULL_NODE) return 0;

Expand All @@ -307,6 +297,12 @@ library RepoTokenList {
totalPresentValue += repoTokenBalanceInBaseAssetPrecision;
}

if (repoTokenToMatch != address(0) && current == repoTokenToMatch) {
// matching a specific repo token and terminate early because the list is sorted
// with no duplicates
break;
}

current = _getNext(listData, current);
}
}
Expand Down
118 changes: 104 additions & 14 deletions src/Strategy.sol
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,15 @@ pragma solidity ^0.8.18;
import {BaseStrategy, ERC20} from "@tokenized-strategy/BaseStrategy.sol";
import {SafeERC20, IERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import {IERC4626} from "@openzeppelin/contracts/interfaces/IERC4626.sol";
import {ReentrancyGuard} from "@openzeppelin/contracts/security/ReentrancyGuard.sol";
import {Pausable} from "@openzeppelin/contracts/security/Pausable.sol";
import {ITermRepoToken} from "./interfaces/term/ITermRepoToken.sol";
import {ITermRepoServicer} from "./interfaces/term/ITermRepoServicer.sol";
import {ITermController} from "./interfaces/term/ITermController.sol";
import {ITermVaultEvents} from "./interfaces/term/ITermVaultEvents.sol";
import {ITermAuctionOfferLocker} from "./interfaces/term/ITermAuctionOfferLocker.sol";
import {ITermRepoCollateralManager} from "./interfaces/term/ITermRepoCollateralManager.sol";
import {ITermDiscountRateAdapter} from "./interfaces/term/ITermDiscountRateAdapter.sol";
import {ITermAuction} from "./interfaces/term/ITermAuction.sol";
import {RepoTokenList, RepoTokenListData} from "./RepoTokenList.sol";
import {TermAuctionList, TermAuctionListData, PendingOffer} from "./TermAuctionList.sol";
Expand All @@ -28,7 +31,7 @@ import {RepoTokenUtils} from "./RepoTokenUtils.sol";

// NOTE: To implement permissioned functions you can use the onlyManagement, onlyEmergencyAuthorized and onlyKeepers modifiers

contract Strategy is BaseStrategy {
contract Strategy is BaseStrategy, Pausable, ReentrancyGuard {
using SafeERC20 for IERC20;
using RepoTokenList for RepoTokenListData;
using TermAuctionList for TermAuctionListData;
Expand All @@ -37,17 +40,36 @@ contract Strategy is BaseStrategy {
error TimeToMaturityAboveThreshold();
error BalanceBelowLiquidityThreshold();
error InsufficientLiquidBalance(uint256 have, uint256 want);
error RepoTokenConcentrationTooHigh(address repoToken);

ITermVaultEvents public immutable TERM_VAULT_EVENT_EMITTER;
uint256 public immutable PURCHASE_TOKEN_PRECISION;
IERC4626 public immutable YEARN_VAULT;

ITermController public termController;
ITermDiscountRateAdapter public discountRateAdapter;
RepoTokenListData internal repoTokenListData;
TermAuctionListData internal termAuctionListData;
uint256 public timeToMaturityThreshold; // seconds
uint256 public liquidityThreshold; // purchase token precision (underlying)
uint256 public auctionRateMarkup; // 1e18 (TODO: check this)
uint256 public repoTokenConcentrationLimit;

function rescueToken(address token, uint256 amount) external onlyManagement {
if (amount > 0) {
IERC20(token).safeTransfer(msg.sender, amount);
}
}

function pause() external onlyManagement {
_pause();
TERM_VAULT_EVENT_EMITTER.emitPaused();
}

function unpause() external onlyManagement {
_unpause();
TERM_VAULT_EVENT_EMITTER.emitUnpaused();
}

// These governance functions should have a different role
function setTermController(address newTermController) external onlyManagement {
Expand Down Expand Up @@ -76,6 +98,18 @@ contract Strategy is BaseStrategy {
repoTokenListData.collateralTokenParams[tokenAddr] = minCollateralRatio;
}

function setRepoTokenConcentrationLimit(uint256 newRepoTokenConcentrationLimit) external onlyManagement {
TERM_VAULT_EVENT_EMITTER.emitRepoTokenConcentrationLimitUpdated(
repoTokenConcentrationLimit, newRepoTokenConcentrationLimit
);
repoTokenConcentrationLimit = newRepoTokenConcentrationLimit;
}

function setDiscountRateAdapter(address newAdapter) external onlyManagement {
TERM_VAULT_EVENT_EMITTER.emitDiscountRateAdapterUpdated(address(discountRateAdapter), newAdapter);
discountRateAdapter = ITermDiscountRateAdapter(newAdapter);
}

function repoTokenHoldings() external view returns (address[] memory) {
return repoTokenListData.holdings();
}
Expand Down Expand Up @@ -141,7 +175,14 @@ contract Strategy is BaseStrategy {
// do not validate if we are simulating with existing repo tokens
if (repoToken != address(0)) {
repoTokenListData.validateRepoToken(ITermRepoToken(repoToken), termController, address(asset));

uint256 repoTokenPrecision = 10**ERC20(repoToken).decimals();
uint256 repoTokenAmountInBaseAssetPrecision =
(ITermRepoToken(repoToken).redemptionValue() * amount * PURCHASE_TOKEN_PRECISION) /
(repoTokenPrecision * RepoTokenUtils.RATE_PRECISION);
_validateRepoTokenConcentration(repoToken, repoTokenAmountInBaseAssetPrecision, 0);
}

return _calculateWeightedMaturity(repoToken, amount, _totalLiquidBalance(address(this)));
}

Expand Down Expand Up @@ -169,7 +210,7 @@ contract Strategy is BaseStrategy {
}

function _sweepAssetAndRedeemRepoTokens(uint256 liquidAmountRequired) private {
termAuctionListData.removeCompleted(repoTokenListData, termController, address(asset));
termAuctionListData.removeCompleted(repoTokenListData, termController, discountRateAdapter, address(asset));
repoTokenListData.removeAndRedeemMaturedTokens();

uint256 underlyingBalance = IERC20(asset).balanceOf(address(this));
Expand All @@ -192,13 +233,14 @@ contract Strategy is BaseStrategy {
return YEARN_VAULT.convertToAssets(YEARN_VAULT.balanceOf(address(this)));
}

// TODO: reentrancy check
function sellRepoToken(address repoToken, uint256 repoTokenAmount) external {
function sellRepoToken(address repoToken, uint256 repoTokenAmount) external whenNotPaused nonReentrant {
require(repoTokenAmount > 0);
require(_totalLiquidBalance(address(this)) > 0);

(uint256 auctionRate, uint256 redemptionTimestamp) = repoTokenListData.validateAndInsertRepoToken(
ITermRepoToken(repoToken),
termController,
discountRateAdapter,
address(asset)
);

Expand Down Expand Up @@ -230,18 +272,38 @@ contract Strategy is BaseStrategy {
revert TimeToMaturityAboveThreshold();
}

liquidBalance -= proceeds;

if (liquidBalance < liquidityThreshold) {
if ((liquidBalance - proceeds) < liquidityThreshold) {
revert BalanceBelowLiquidityThreshold();
}

_validateRepoTokenConcentration(repoToken, repoTokenAmountInBaseAssetPrecision, proceeds);

// withdraw from underlying vault
_withdrawAsset(proceeds);

IERC20(repoToken).safeTransferFrom(msg.sender, address(this), repoTokenAmount);
IERC20(asset).safeTransfer(msg.sender, proceeds);
}

function _validateRepoTokenConcentration(
address repoToken,
uint256 repoTokenAmountInBaseAssetPrecision,
uint256 liquidBalanceToRemove
) private view {
// _repoTokenValue returns asset precision
uint256 repoTokenValue = getRepoTokenValue(repoToken) + repoTokenAmountInBaseAssetPrecision;
uint256 totalAsseValue = _totalAssetValue() + repoTokenAmountInBaseAssetPrecision - liquidBalanceToRemove;

// repoTokenConcentrationLimit is in 1e18 precision
repoTokenValue = repoTokenValue * 1e18 / PURCHASE_TOKEN_PRECISION;
totalAsseValue = totalAsseValue * 1e18 / PURCHASE_TOKEN_PRECISION;

uint256 repoTokenConcentration = totalAsseValue == 0 ? 0 : repoTokenValue * 1e18 / totalAsseValue;

if (repoTokenConcentration > repoTokenConcentrationLimit) {
revert RepoTokenConcentrationTooHigh(repoToken);
}
}

function deleteAuctionOffers(address termAuction, bytes32[] calldata offerIds) external onlyManagement {
if (!termController.isTermDeployed(termAuction)) {
Expand All @@ -253,7 +315,7 @@ contract Strategy is BaseStrategy {

offerLocker.unlockOffers(offerIds);

termAuctionListData.removeCompleted(repoTokenListData, termController, address(asset));
termAuctionListData.removeCompleted(repoTokenListData, termController, discountRateAdapter, address(asset));

_sweepAssetAndRedeemRepoTokens(0);
}
Expand Down Expand Up @@ -291,7 +353,7 @@ contract Strategy is BaseStrategy {
bytes32 idHash,
bytes32 offerPriceHash,
uint256 purchaseTokenAmount
) external onlyManagement returns (bytes32[] memory offerIds) {
) external whenNotPaused nonReentrant onlyManagement returns (bytes32[] memory offerIds) {
require(purchaseTokenAmount > 0);

if (!termController.isTermDeployed(termAuction)) {
Expand All @@ -305,6 +367,8 @@ contract Strategy is BaseStrategy {
// validate purchase token and min collateral ratio
repoTokenListData.validateRepoToken(ITermRepoToken(repoToken), termController, address(asset));

_validateRepoTokenConcentration(repoToken, purchaseTokenAmount, 0);

ITermAuctionOfferLocker offerLocker = ITermAuctionOfferLocker(auction.termAuctionOfferLocker());
require(
block.timestamp > offerLocker.auctionStartTime()
Expand Down Expand Up @@ -348,6 +412,20 @@ contract Strategy is BaseStrategy {
// no change in offer amount, do nothing
}

{
uint256 repoTokenPrecision = 10**ERC20(repoToken).decimals();
uint256 offerAmount = RepoTokenUtils.purchaseToRepoPrecision(
repoTokenPrecision, PURCHASE_TOKEN_PRECISION, purchaseTokenAmount
);
uint256 resultingWeightedTimeToMaturity = _removeRedeemAndCalculateWeightedMaturity(
repoToken, offerAmount, liquidBalance - actualPurchaseTokenAmount
);

if (resultingWeightedTimeToMaturity > timeToMaturityThreshold) {
revert TimeToMaturityAboveThreshold();
}
}

ITermAuctionOfferLocker.TermAuctionOfferSubmission memory offer;

offer.id = currentOfferAmount > 0 ? offerId : idHash;
Expand Down Expand Up @@ -424,23 +502,35 @@ contract Strategy is BaseStrategy {
function totalLiquidBalance() external view returns (uint256) {
return _totalLiquidBalance(address(this));
}

function getRepoTokenValue(address repoToken) public view returns (uint256) {
return repoTokenListData.getPresentValue(PURCHASE_TOKEN_PRECISION, repoToken) +
termAuctionListData.getPresentValue(
repoTokenListData, discountRateAdapter, PURCHASE_TOKEN_PRECISION, repoToken
);
}

function _totalAssetValue() internal view returns (uint256 totalValue) {
return _totalLiquidBalance(address(this)) +
repoTokenListData.getPresentValue(PURCHASE_TOKEN_PRECISION) +
termAuctionListData.getPresentValue(repoTokenListData, termController, PURCHASE_TOKEN_PRECISION);
repoTokenListData.getPresentValue(PURCHASE_TOKEN_PRECISION, address(0)) +
termAuctionListData.getPresentValue(
repoTokenListData, discountRateAdapter, PURCHASE_TOKEN_PRECISION, address(0)
);
}

constructor(
address _asset,
string memory _name,
address _yearnVault,
address _discountRateAdapter,
address _eventEmitter
) BaseStrategy(_asset, _name) {
YEARN_VAULT = IERC4626(_yearnVault);
TERM_VAULT_EVENT_EMITTER = ITermVaultEvents(_eventEmitter);
PURCHASE_TOKEN_PRECISION = 10**ERC20(asset).decimals();

discountRateAdapter = ITermDiscountRateAdapter(_discountRateAdapter);

IERC20(_asset).safeApprove(_yearnVault, type(uint256).max);
}

Expand All @@ -459,7 +549,7 @@ contract Strategy is BaseStrategy {
* @param _amount The amount of 'asset' that the strategy can attempt
* to deposit in the yield source.
*/
function _deployFunds(uint256 _amount) internal override {
function _deployFunds(uint256 _amount) internal override whenNotPaused {
_sweepAssetAndRedeemRepoTokens(0);
}

Expand All @@ -484,7 +574,7 @@ contract Strategy is BaseStrategy {
*
* @param _amount, The amount of 'asset' to be freed.
*/
function _freeFunds(uint256 _amount) internal override {
function _freeFunds(uint256 _amount) internal override whenNotPaused {
_sweepAssetAndRedeemRepoTokens(_amount);
}

Expand Down Expand Up @@ -513,7 +603,7 @@ contract Strategy is BaseStrategy {
function _harvestAndReport()
internal
override
returns (uint256 _totalAssets)
whenNotPaused returns (uint256 _totalAssets)
{
_sweepAssetAndRedeemRepoTokens(0);
return _totalAssetValue();
Expand Down
Loading
Loading