Skip to content

Commit

Permalink
adding repo concentration limit
Browse files Browse the repository at this point in the history
  • Loading branch information
0xddong committed Aug 7, 2024
1 parent efcb6c7 commit bb9cee5
Show file tree
Hide file tree
Showing 7 changed files with 180 additions and 13 deletions.
12 changes: 9 additions & 3 deletions src/RepoTokenList.sol
Original file line number Diff line number Diff line change
Expand Up @@ -213,8 +213,7 @@ library RepoTokenList {
ITermRepoToken repoToken,
ITermController termController,
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 Down Expand Up @@ -283,7 +282,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 +307,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
88 changes: 79 additions & 9 deletions src/Strategy.sol
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ 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";
Expand All @@ -28,7 +30,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,6 +39,7 @@ 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;
Expand All @@ -48,6 +51,23 @@ contract Strategy is BaseStrategy {
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 +96,13 @@ contract Strategy is BaseStrategy {
repoTokenListData.collateralTokenParams[tokenAddr] = minCollateralRatio;
}

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

function repoTokenHoldings() external view returns (address[] memory) {
return repoTokenListData.holdings();
}
Expand Down Expand Up @@ -141,7 +168,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 @@ -192,8 +226,7 @@ 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);

Expand Down Expand Up @@ -237,12 +270,38 @@ contract Strategy is BaseStrategy {
revert BalanceBelowLiquidityThreshold();
}

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 Down Expand Up @@ -292,7 +351,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 @@ -306,6 +365,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 @@ -439,11 +500,20 @@ 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, termController, 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, termController, PURCHASE_TOKEN_PRECISION, address(0)
);
}

constructor(
Expand Down Expand Up @@ -474,7 +544,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 @@ -499,7 +569,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 @@ -528,7 +598,7 @@ contract Strategy is BaseStrategy {
function _harvestAndReport()
internal
override
returns (uint256 _totalAssets)
whenNotPaused returns (uint256 _totalAssets)
{
_sweepAssetAndRedeemRepoTokens(0);
return _totalAssetValue();
Expand Down
8 changes: 7 additions & 1 deletion src/TermAuctionList.sol
Original file line number Diff line number Diff line change
Expand Up @@ -175,7 +175,8 @@ library TermAuctionList {
TermAuctionListData storage listData,
RepoTokenListData storage repoTokenListData,
ITermController termController,
uint256 purchaseTokenPrecision
uint256 purchaseTokenPrecision,
address repoTokenToMatch
) internal view returns (uint256 totalValue) {
if (listData.head == NULL_NODE) return 0;

Expand All @@ -184,6 +185,11 @@ library TermAuctionList {
for (uint256 i; i < offers.length; i++) {
PendingOfferMemory memory offer = offers[i];

// filter by repoTokenToMatch if necessary
if (repoTokenToMatch != address(0) && offer.repoToken != repoTokenToMatch) {
continue;
}

uint256 offerAmount = offer.offerLocker.lockedOffer(offer.offerId).amount;

/// @dev offer processed, but auctionClosed not yet called and auction is new so repoToken not on List and wont be picked up
Expand Down
12 changes: 12 additions & 0 deletions src/TermVaultEventEmitter.sol
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,18 @@ contract TermVaultEventEmitter is Initializable, UUPSUpgradeable, AccessControlU
emit MinCollateralRatioUpdated(collateral, minCollateralRatio);
}

function emitRepoTokenConcentrationLimitUpdated(uint256 oldLimit, uint256 newLimit) external onlyRole(VAULT_CONTRACT) {
emit RepoTokenConcentrationLimitUpdated(oldLimit, newLimit);
}

function emitPaused() external onlyRole(VAULT_CONTRACT) {
emit Paused();
}

function emitUnpaused() external onlyRole(VAULT_CONTRACT) {
emit Unpaused();
}

// ========================================================================
// = Admin ===============================================================
// ========================================================================
Expand Down
12 changes: 12 additions & 0 deletions src/interfaces/term/ITermVaultEvents.sol
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,12 @@ interface ITermVaultEvents {

event MinCollateralRatioUpdated(address collateral, uint256 minCollateralRatio);

event RepoTokenConcentrationLimitUpdated(uint256 oldLimit, uint256 newLimit);

event Paused();

event Unpaused();

function emitTermControllerUpdated(address oldController, address newController) external;

function emitTimeToMaturityThresholdUpdated(uint256 oldThreshold, uint256 newThreshold) external;
Expand All @@ -21,4 +27,10 @@ interface ITermVaultEvents {
function emitAuctionRateMarkupUpdated(uint256 oldMarkup, uint256 newMarkup) external;

function emitMinCollateralRatioUpdated(address collateral, uint256 minCollateralRatio) external;

function emitRepoTokenConcentrationLimitUpdated(uint256 oldLimit, uint256 newLimit) external;

function emitPaused() external;

function emitUnpaused() external;
}
1 change: 1 addition & 0 deletions src/test/TestUSDCOffers.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ contract TestUSDCSubmitOffer is Setup {
vm.startPrank(management);
termStrategy.setCollateralTokenParams(address(mockCollateral), 0.5e18);
termStrategy.setTimeToMaturityThreshold(3 weeks);
termStrategy.setRepoTokenConcentrationLimit(1e18);
vm.stopPrank();

// start with some initial funds
Expand Down
60 changes: 60 additions & 0 deletions src/test/TestUSDCSellRepoToken.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ contract TestUSDCSellRepoToken is Setup {
vm.startPrank(management);
termStrategy.setCollateralTokenParams(address(mockCollateral), 0.5e18);
termStrategy.setTimeToMaturityThreshold(10 weeks);
termStrategy.setRepoTokenConcentrationLimit(1e18);
vm.stopPrank();

}
Expand Down Expand Up @@ -161,6 +162,15 @@ contract TestUSDCSellRepoToken is Setup {
_sellRepoTokens(tokens, amounts, true, true, err);
}

function _sell1RepoTokenNoMintExpectRevert(MockTermRepoToken rt1, uint256 amount1, bytes memory err) private {
address[] memory tokens = new address[](1);
tokens[0] = address(rt1);
uint256[] memory amounts = new uint256[](1);
amounts[0] = amount1;

_sellRepoTokens(tokens, amounts, true, false, err);
}

function _sell3RepoTokens(
MockTermRepoToken rt1,
uint256 amount1,
Expand Down Expand Up @@ -509,4 +519,54 @@ contract TestUSDCSellRepoToken is Setup {

console.log("totalAssetValue", termStrategy.totalAssetValue());
}

function testConcentrationLimitFailure() public {
address testDepositor = vm.addr(0x111111);
uint256 depositAmount = 1000e6;

mockUSDC.mint(testDepositor, depositAmount);

vm.startPrank(testDepositor);
mockUSDC.approve(address(termStrategy), type(uint256).max);
IERC4626(address(termStrategy)).deposit(depositAmount, testDepositor);
vm.stopPrank();

vm.expectRevert("!management");
termStrategy.setRepoTokenConcentrationLimit(0.4e18);

// Set to 40%
vm.prank(management);
termStrategy.setRepoTokenConcentrationLimit(0.4e18);

_sell1RepoTokenNoMintExpectRevert(
repoToken2Week,
500e18,
abi.encodeWithSelector(Strategy.RepoTokenConcentrationTooHigh.selector, address(repoToken2Week))
);
}

function testPausing() public {
address testDepositor = vm.addr(0x111111);
uint256 depositAmount = 1000e6;

mockUSDC.mint(testDepositor, depositAmount);

vm.expectRevert("!management");
termStrategy.pause();

vm.prank(management);
termStrategy.pause();

vm.startPrank(testDepositor);
mockUSDC.approve(address(termStrategy), type(uint256).max);
vm.expectRevert("Pausable: paused");
IERC4626(address(termStrategy)).deposit(depositAmount, testDepositor);
vm.stopPrank();

_sell1RepoTokenExpectRevert(
repoToken2Week,
2e18,
"Pausable: paused"
);
}
}

0 comments on commit bb9cee5

Please sign in to comment.