From 2efc4da95964e84b45bdd3f2a638737f66726481 Mon Sep 17 00:00:00 2001 From: Andrew Zhou Date: Thu, 5 Sep 2024 16:57:12 -0700 Subject: [PATCH 01/20] delete sweep assets and integrate token transfer into redeem repo tokens --- src/Strategy.sol | 42 ++++++++---------------------------------- 1 file changed, 8 insertions(+), 34 deletions(-) diff --git a/src/Strategy.sol b/src/Strategy.sol index cb3a4860..04cecd94 100644 --- a/src/Strategy.sol +++ b/src/Strategy.sol @@ -652,16 +652,6 @@ contract Strategy is BaseStrategy, Pausable, ReentrancyGuard { (cumulativeAmount + liquidBalance); } - /** - * @notice Deposits all available asset tokens into the liquid vault - * - * @dev This function transfers the entire balance of the asset token held by this contract - * into the associated liquid vault. - */ - function _sweepAsset() private { - YEARN_VAULT.deposit(IERC20(asset).balanceOf(address(this)), address(this)); - } - /** * @notice Checks if a term contract is marked as deployed in either the current or previous term controller * @param termContract The address of the term contract to check @@ -689,36 +679,26 @@ contract Strategy is BaseStrategy, Pausable, ReentrancyGuard { * optimizing asset allocation. */ function _redeemRepoTokens(uint256 liquidAmountRequired) private { - uint256 liquidityBefore = IERC20(asset).balanceOf(address(this)); - // Remove completed auction offers - termAuctionListData.removeCompleted( - repoTokenListData, - discountRateAdapter, - address(asset) - ); + termAuctionListData.removeCompleted(repoTokenListData, discountRateAdapter, address(asset)); // Remove and redeem matured repoTokens repoTokenListData.removeAndRedeemMaturedTokens(); - uint256 liquidityAfter = IERC20(asset).balanceOf(address(this)); - uint256 liquidityDiff = liquidityAfter - liquidityBefore; + uint256 liquidity = IERC20(asset).balanceOf(address(this)); // Deposit excess underlying balance into Yearn Vault - if (liquidityDiff > liquidAmountRequired) { + if (liquidity > liquidAmountRequired) { unchecked { - YEARN_VAULT.deposit( - liquidityDiff - liquidAmountRequired, - address(this) - ); + YEARN_VAULT.deposit(liquidity - liquidAmountRequired, address(this)); } - // Withdraw shortfall from Yearn Vault to meet required liquidity - } else if (liquidityDiff < liquidAmountRequired) { + // Withdraw shortfall from Yearn Vault to meet required liquidity + } else if (liquidity < liquidAmountRequired) { unchecked { - _withdrawAsset(liquidAmountRequired - liquidityDiff); + _withdrawAsset(liquidAmountRequired - liquidity); } } - } +} /*////////////////////////////////////////////////////////////// STRATEGIST FUNCTIONS @@ -801,7 +781,6 @@ contract Strategy is BaseStrategy, Pausable, ReentrancyGuard { ); // Sweep assets, redeem matured repoTokens and ensure liquid balances up to date - _sweepAsset(); _redeemRepoTokens(0); bytes32 offerId = _generateOfferId(idHash, address(offerLocker)); @@ -962,7 +941,6 @@ contract Strategy is BaseStrategy, Pausable, ReentrancyGuard { ); // Sweep any remaining assets and redeem repoTokens - _sweepAsset(); _redeemRepoTokens(0); } @@ -983,7 +961,6 @@ contract Strategy is BaseStrategy, Pausable, ReentrancyGuard { * @notice Close the auction */ function auctionClosed() external { - _sweepAsset(); _redeemRepoTokens(0); } @@ -1017,7 +994,6 @@ contract Strategy is BaseStrategy, Pausable, ReentrancyGuard { ); // Sweep assets and redeem repoTokens, if needed - _sweepAsset(); _redeemRepoTokens(0); // Retrieve total liquid balance and ensure it's greater than zero @@ -1127,7 +1103,6 @@ contract Strategy is BaseStrategy, Pausable, ReentrancyGuard { revert DepositPaused(); } - _sweepAsset(); _redeemRepoTokens(0); } @@ -1184,7 +1159,6 @@ contract Strategy is BaseStrategy, Pausable, ReentrancyGuard { whenNotPaused returns (uint256 _totalAssets) { - _sweepAsset(); _redeemRepoTokens(0); return _totalAssetValue(_totalLiquidBalance()); } From 1e6e4d2c987b76c32ac3f578a3827a260b7a48a9 Mon Sep 17 00:00:00 2001 From: Andrew Zhou Date: Mon, 9 Sep 2024 17:52:50 -0700 Subject: [PATCH 02/20] refactor calculations to use utils --- src/RepoTokenList.sol | 20 +++--- src/RepoTokenUtils.sol | 8 ++- src/Strategy.sol | 62 ++++++++++--------- src/TermAuctionList.sol | 8 ++- src/TermDiscountRateAdapter.sol | 21 ++++++- .../term/ITermDiscountRateAdapter.sol | 1 + src/test/utils/Setup.sol | 2 +- 7 files changed, 76 insertions(+), 46 deletions(-) diff --git a/src/RepoTokenList.sol b/src/RepoTokenList.sol index 1767a323..f553b1ef 100644 --- a/src/RepoTokenList.sol +++ b/src/RepoTokenList.sol @@ -113,6 +113,7 @@ library RepoTokenList { /** * @notice This function calculates the cumulative weighted time to maturity and cumulative amount of all repoTokens in the list. * @param listData The list data + * @param discountRateAdapter The discount rate adapter * @param repoToken The address of the repoToken (optional) * @param repoTokenAmount The amount of the repoToken (optional) * @param purchaseTokenPrecision The precision of the purchase token @@ -127,6 +128,7 @@ library RepoTokenList { */ function getCumulativeRepoTokenData( RepoTokenListData storage listData, + ITermDiscountRateAdapter discountRateAdapter, address repoToken, uint256 repoTokenAmount, uint256 purchaseTokenPrecision @@ -148,12 +150,10 @@ library RepoTokenList { } // Convert the repo token balance to base asset precision - uint256 redemptionValue = ITermRepoToken(current).redemptionValue(); - uint256 repoTokenPrecision = 10**ERC20(current).decimals(); - uint256 repoTokenBalanceInBaseAssetPrecision = - (redemptionValue * repoTokenBalance * purchaseTokenPrecision) / - (repoTokenPrecision * RepoTokenUtils.RATE_PRECISION); + RepoTokenUtils.getNormalizedRepoTokenAmount( + current, repoTokenBalance, purchaseTokenPrecision, discountRateAdapter.repoRedemptionHaircut(current) + ); // Calculate the weighted time to maturity uint256 weightedTimeToMaturity = getRepoTokenWeightedTimeToMaturity( @@ -173,6 +173,7 @@ library RepoTokenList { /** * @notice Get the present value of repoTokens * @param listData The list data + * @param discountRateAdapter The discount rate adapter * @param purchaseTokenPrecision The precision of the purchase token * @param repoTokenToMatch The address of the repoToken to match (optional) * @return totalPresentValue The total present value of the repoTokens @@ -186,6 +187,7 @@ library RepoTokenList { */ function getPresentValue( RepoTokenListData storage listData, + ITermDiscountRateAdapter discountRateAdapter, uint256 purchaseTokenPrecision, address repoTokenToMatch ) internal view returns (uint256 totalPresentValue) { @@ -204,14 +206,14 @@ library RepoTokenList { uint256 currentMaturity = getRepoTokenMaturity(current); uint256 repoTokenBalance = ITermRepoToken(current).balanceOf(address(this)); - uint256 repoTokenPrecision = 10**ERC20(current).decimals(); - uint256 discountRate = listData.discountRates[current]; + uint256 discountRate = discountRateAdapter.getDiscountRate(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); + RepoTokenUtils.getNormalizedRepoTokenAmount( + current, repoTokenBalance, purchaseTokenPrecision, discountRateAdapter.repoRedemptionHaircut(current) + ); // Calculate present value based on maturity if (currentMaturity > block.timestamp) { diff --git a/src/RepoTokenUtils.sol b/src/RepoTokenUtils.sol index 1376f403..ae2d8417 100644 --- a/src/RepoTokenUtils.sol +++ b/src/RepoTokenUtils.sol @@ -78,17 +78,19 @@ library RepoTokenUtils { * @param repoToken The address of the repoToken * @param repoTokenAmount The amount of the repoToken * @param purchaseTokenPrecision The precision of the purchase token + * @param repoRedemptionHaircut The haircut to be applied to the repoToken for bad debt * @return repoTokenAmountInBaseAssetPrecision The normalized amount of the repoToken in base asset precision */ function getNormalizedRepoTokenAmount( address repoToken, uint256 repoTokenAmount, - uint256 purchaseTokenPrecision + uint256 purchaseTokenPrecision, + uint256 repoRedemptionHaircut ) internal view returns (uint256 repoTokenAmountInBaseAssetPrecision) { uint256 repoTokenPrecision = 10**ERC20(repoToken).decimals(); uint256 redemptionValue = ITermRepoToken(repoToken).redemptionValue(); repoTokenAmountInBaseAssetPrecision = - (redemptionValue * repoTokenAmount * purchaseTokenPrecision) / - (repoTokenPrecision * RepoTokenUtils.RATE_PRECISION); + (redemptionValue * repoRedemptionHaircut * repoTokenAmount * purchaseTokenPrecision) / + (repoTokenPrecision * RATE_PRECISION * 1e18); } } \ No newline at end of file diff --git a/src/Strategy.sol b/src/Strategy.sol index 57e41094..50187b4d 100644 --- a/src/Strategy.sol +++ b/src/Strategy.sol @@ -351,15 +351,14 @@ contract Strategy is BaseStrategy, Pausable, ReentrancyGuard { ITermRepoToken(repoToken), address(asset) ); - + uint256 discountRate = discountRateAdapter.getDiscountRate(repoToken); - uint256 repoTokenPrecision = 10 ** ERC20(repoToken).decimals(); - repoTokenAmountInBaseAssetPrecision = (ITermRepoToken( - repoToken - ).redemptionValue() * - amount * - PURCHASE_TOKEN_PRECISION) / - (repoTokenPrecision * RepoTokenUtils.RATE_PRECISION); + repoTokenAmountInBaseAssetPrecision = RepoTokenUtils.getNormalizedRepoTokenAmount( + repoToken, + amount, + PURCHASE_TOKEN_PRECISION, + discountRateAdapter.repoRedemptionHaircut(repoToken) + ); proceeds = RepoTokenUtils.calculatePresentValue( repoTokenAmountInBaseAssetPrecision, PURCHASE_TOKEN_PRECISION, @@ -398,15 +397,16 @@ contract Strategy is BaseStrategy, Pausable, ReentrancyGuard { address repoToken, uint256 discountRate, uint256 amount - ) external view returns (uint256) { + ) public view returns (uint256) { (uint256 redemptionTimestamp, , , ) = ITermRepoToken(repoToken) .config(); - uint256 repoTokenPrecision = 10 ** ERC20(repoToken).decimals(); - uint256 repoTokenAmountInBaseAssetPrecision = (ITermRepoToken(repoToken) - .redemptionValue() * - amount * - PURCHASE_TOKEN_PRECISION) / - (repoTokenPrecision * RepoTokenUtils.RATE_PRECISION); + uint256 repoTokenAmountInBaseAssetPrecision = RepoTokenUtils + .getNormalizedRepoTokenAmount( + repoToken, + amount, + PURCHASE_TOKEN_PRECISION, + discountRateAdapter.repoRedemptionHaircut(repoToken) + ); return RepoTokenUtils.calculatePresentValue( repoTokenAmountInBaseAssetPrecision, @@ -428,11 +428,10 @@ contract Strategy is BaseStrategy, Pausable, ReentrancyGuard { function getRepoTokenHoldingValue( address repoToken ) public view returns (uint256) { + uint256 repoTokenBalance = ITermRepoToken(repoToken).balanceOf(address(this)); + uint256 repoTokenPresentValue = calculateRepoTokenPresentValue(repoToken, discountRateAdapter.getDiscountRate(repoToken), repoTokenBalance); return - repoTokenListData.getPresentValue( - PURCHASE_TOKEN_PRECISION, - repoToken - ) + + repoTokenPresentValue + termAuctionListData.getPresentValue( repoTokenListData, discountRateAdapter, @@ -489,6 +488,7 @@ contract Strategy is BaseStrategy, Pausable, ReentrancyGuard { return liquidBalance + repoTokenListData.getPresentValue( + discountRateAdapter, PURCHASE_TOKEN_PRECISION, address(0) ) + @@ -596,6 +596,7 @@ contract Strategy is BaseStrategy, Pausable, ReentrancyGuard { uint256 cumulativeRepoTokenAmount, bool foundInRepoTokenList ) = repoTokenListData.getCumulativeRepoTokenData( + discountRateAdapter, repoToken, repoTokenAmount, PURCHASE_TOKEN_PRECISION @@ -611,6 +612,7 @@ contract Strategy is BaseStrategy, Pausable, ReentrancyGuard { bool foundInOfferList ) = termAuctionListData.getCumulativeOfferData( repoTokenListData, + discountRateAdapter, repoToken, repoTokenAmount, PURCHASE_TOKEN_PRECISION @@ -625,11 +627,13 @@ contract Strategy is BaseStrategy, Pausable, ReentrancyGuard { !foundInOfferList && repoToken != address(0) ) { + uint256 repoRedemptionHaircut = discountRateAdapter.repoRedemptionHaircut(repoToken); uint256 repoTokenAmountInBaseAssetPrecision = RepoTokenUtils .getNormalizedRepoTokenAmount( repoToken, repoTokenAmount, - PURCHASE_TOKEN_PRECISION + PURCHASE_TOKEN_PRECISION, + repoRedemptionHaircut ); cumulativeAmount += repoTokenAmountInBaseAssetPrecision; @@ -1005,7 +1009,7 @@ contract Strategy is BaseStrategy, Pausable, ReentrancyGuard { } // Validate and insert the repoToken into the list, retrieve auction rate and redemption timestamp - (uint256 discountRate, uint256 redemptionTimestamp) = repoTokenListData + (, uint256 redemptionTimestamp) = repoTokenListData .validateAndInsertRepoToken( ITermRepoToken(repoToken), discountRateAdapter, @@ -1020,13 +1024,15 @@ contract Strategy is BaseStrategy, Pausable, ReentrancyGuard { uint256 liquidBalance = _totalLiquidBalance(); require(liquidBalance > 0); - // Calculate the repoToken amount in base asset precision - uint256 repoTokenPrecision = 10 ** ERC20(repoToken).decimals(); - uint256 repoTokenAmountInBaseAssetPrecision = (ITermRepoToken(repoToken) - .redemptionValue() * - repoTokenAmount * - PURCHASE_TOKEN_PRECISION) / - (repoTokenPrecision * RepoTokenUtils.RATE_PRECISION); + uint256 discountRate = discountRateAdapter.getDiscountRate(repoToken); + + // Calculate the repoToken amount in base asset precision + uint256 repoTokenAmountInBaseAssetPrecision = RepoTokenUtils.getNormalizedRepoTokenAmount( + repoToken, + repoTokenAmount, + PURCHASE_TOKEN_PRECISION, + discountRateAdapter.repoRedemptionHaircut(repoToken) + ); // Calculate the proceeds from selling the repoToken uint256 proceeds = RepoTokenUtils.calculatePresentValue( diff --git a/src/TermAuctionList.sol b/src/TermAuctionList.sol index 672b3b75..e0e8fd04 100644 --- a/src/TermAuctionList.sol +++ b/src/TermAuctionList.sol @@ -291,7 +291,8 @@ library TermAuctionList { uint256 repoTokenAmountInBaseAssetPrecision = RepoTokenUtils.getNormalizedRepoTokenAmount( offer.repoToken, ITermRepoToken(offer.repoToken).balanceOf(address(this)), - purchaseTokenPrecision + purchaseTokenPrecision, + discountRateAdapter.repoRedemptionHaircut(offer.repoToken) ); totalValue += RepoTokenUtils.calculatePresentValue( repoTokenAmountInBaseAssetPrecision, @@ -316,6 +317,7 @@ library TermAuctionList { * @notice Get cumulative offer data for a specified repoToken * @param listData The list data * @param repoTokenListData The repoToken list data + * @param discountRateAdapter The discount rate adapter * @param repoToken The address of the repoToken (optional) * @param newOfferAmount The new offer amount for the specified repoToken * @param purchaseTokenPrecision The precision of the purchase token @@ -331,6 +333,7 @@ library TermAuctionList { function getCumulativeOfferData( TermAuctionListData storage listData, RepoTokenListData storage repoTokenListData, + ITermDiscountRateAdapter discountRateAdapter, address repoToken, uint256 newOfferAmount, uint256 purchaseTokenPrecision @@ -361,7 +364,8 @@ library TermAuctionList { offerAmount = RepoTokenUtils.getNormalizedRepoTokenAmount( offer.repoToken, ITermRepoToken(offer.repoToken).balanceOf(address(this)), - purchaseTokenPrecision + purchaseTokenPrecision, + discountRateAdapter.repoRedemptionHaircut(offer.repoToken) ); _markRepoTokenAsSeen(offers, offer.repoToken); diff --git a/src/TermDiscountRateAdapter.sol b/src/TermDiscountRateAdapter.sol index e6610ee7..ac8f5a48 100644 --- a/src/TermDiscountRateAdapter.sol +++ b/src/TermDiscountRateAdapter.sol @@ -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 ORACLE_ROLE = keccak256("ORACLE_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 oracleWallet_ The address of the oracle wallet */ - constructor(address termController_) { + constructor(address termController_, address oracleWallet_) { TERM_CONTROLLER = ITermController(termController_); + _grantRole(ORACLE_ROLE, oracleWallet_); } /** @@ -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(ORACLE_ROLE) { + repoRedemptionHaircut[repoToken] = haircut; + } +} \ No newline at end of file diff --git a/src/interfaces/term/ITermDiscountRateAdapter.sol b/src/interfaces/term/ITermDiscountRateAdapter.sol index 97fe4019..feba49c3 100644 --- a/src/interfaces/term/ITermDiscountRateAdapter.sol +++ b/src/interfaces/term/ITermDiscountRateAdapter.sol @@ -2,5 +2,6 @@ pragma solidity ^0.8.18; interface ITermDiscountRateAdapter { + function repoRedemptionHaircut(address) external view returns (uint256); function getDiscountRate(address repoToken) external view returns (uint256); } diff --git a/src/test/utils/Setup.sol b/src/test/utils/Setup.sol index a2dc0fe4..a81ed406 100644 --- a/src/test/utils/Setup.sol +++ b/src/test/utils/Setup.sol @@ -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), adminWallet); termVaultEventEmitterImpl = new TermVaultEventEmitter(); termVaultEventEmitter = TermVaultEventEmitter(address(new ERC1967Proxy(address(termVaultEventEmitterImpl), ""))); mockYearnVault = new ERC4626Mock(address(asset)); From 9dc10e49d4c180c8c6f09010b32c7a05e95eb724 Mon Sep 17 00:00:00 2001 From: Andrew Zhou Date: Mon, 9 Sep 2024 17:55:33 -0700 Subject: [PATCH 03/20] revert repo token holding value --- src/Strategy.sol | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/Strategy.sol b/src/Strategy.sol index 50187b4d..fa65baf7 100644 --- a/src/Strategy.sol +++ b/src/Strategy.sol @@ -428,10 +428,12 @@ contract Strategy is BaseStrategy, Pausable, ReentrancyGuard { function getRepoTokenHoldingValue( address repoToken ) public view returns (uint256) { - uint256 repoTokenBalance = ITermRepoToken(repoToken).balanceOf(address(this)); - uint256 repoTokenPresentValue = calculateRepoTokenPresentValue(repoToken, discountRateAdapter.getDiscountRate(repoToken), repoTokenBalance); - return - repoTokenPresentValue + + return + repoTokenListData.getPresentValue( + discountRateAdapter, + PURCHASE_TOKEN_PRECISION, + repoToken + ) + termAuctionListData.getPresentValue( repoTokenListData, discountRateAdapter, From a127c5cf5946ee1af5e7449592f461a5ada85039 Mon Sep 17 00:00:00 2001 From: Andrew Zhou Date: Mon, 9 Sep 2024 18:03:36 -0700 Subject: [PATCH 04/20] refactor a new oracle wallet in test setup --- src/test/utils/Setup.sol | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/test/utils/Setup.sol b/src/test/utils/Setup.sol index a81ed406..fea7c67a 100644 --- a/src/test/utils/Setup.sol +++ b/src/test/utils/Setup.sol @@ -50,6 +50,7 @@ contract Setup is ExtendedTest, IEvents { address public performanceFeeRecipient = address(3); address public adminWallet = address(111); address public devopsWallet = address(222); + address public oracleWallet = address(333); // Address of the real deployed Factory address public factory; @@ -95,7 +96,7 @@ contract Setup is ExtendedTest, IEvents { vm.etch(0xBB51273D6c746910C7C06fe718f30c936170feD0, address(tokenizedStrategy).code); termController = new MockTermController(); - discountRateAdapter = new TermDiscountRateAdapter(address(termController), adminWallet); + discountRateAdapter = new TermDiscountRateAdapter(address(termController), oracleWallet); termVaultEventEmitterImpl = new TermVaultEventEmitter(); termVaultEventEmitter = TermVaultEventEmitter(address(new ERC1967Proxy(address(termVaultEventEmitterImpl), ""))); mockYearnVault = new ERC4626Mock(address(asset)); From 916b05d54f782342f97db6d9fddefe84380227e9 Mon Sep 17 00:00:00 2001 From: Andrew Zhou Date: Mon, 9 Sep 2024 18:27:56 -0700 Subject: [PATCH 05/20] miscellaneous audit fixes --- src/RepoTokenList.sol | 2 +- src/RepoTokenUtils.sol | 34 ---------------- src/Strategy.sol | 52 +++++------------------- src/TermAuctionList.sol | 7 ++-- src/TermVaultEventEmitter.sol | 1 + src/interfaces/term/ITermVaultEvents.sol | 2 + src/periphery/StrategyAprOracle.sol | 2 +- 7 files changed, 19 insertions(+), 81 deletions(-) diff --git a/src/RepoTokenList.sol b/src/RepoTokenList.sol index 1767a323..4339d388 100644 --- a/src/RepoTokenList.sol +++ b/src/RepoTokenList.sol @@ -324,7 +324,7 @@ library RepoTokenList { (redemptionTimestamp, purchaseToken, , collateralManager) = repoToken.config(); // Validate purchase token - if (purchaseToken != address(asset)) { + if (purchaseToken != asset) { revert InvalidRepoToken(address(repoToken)); } diff --git a/src/RepoTokenUtils.sol b/src/RepoTokenUtils.sol index 1376f403..6ed920d4 100644 --- a/src/RepoTokenUtils.sol +++ b/src/RepoTokenUtils.sol @@ -12,40 +12,6 @@ library RepoTokenUtils { uint256 public constant THREESIXTY_DAYCOUNT_SECONDS = 360 days; uint256 public constant RATE_PRECISION = 1e18; - /*////////////////////////////////////////////////////////////// - PURE FUNCTIONS - //////////////////////////////////////////////////////////////*/ - - /** - * @notice Convert repoToken amount to purchase token precision - * @param repoTokenPrecision The precision of the repoToken - * @param purchaseTokenPrecision The precision of the purchase token - * @param purchaseTokenAmountInRepoPrecision The amount of purchase token in repoToken precision - * @return The amount in purchase token precision - */ - function repoToPurchasePrecision( - uint256 repoTokenPrecision, - uint256 purchaseTokenPrecision, - uint256 purchaseTokenAmountInRepoPrecision - ) internal pure returns (uint256) { - return (purchaseTokenAmountInRepoPrecision * purchaseTokenPrecision) / repoTokenPrecision; - } - - /** - * @notice Convert purchase token amount to repoToken precision - * @param repoTokenPrecision The precision of the repoToken - * @param purchaseTokenPrecision The precision of the purchase token - * @param repoTokenAmount The amount of repoToken - * @return The amount in repoToken precision - */ - function purchaseToRepoPrecision( - uint256 repoTokenPrecision, - uint256 purchaseTokenPrecision, - uint256 repoTokenAmount - ) internal pure returns (uint256) { - return (repoTokenAmount * repoTokenPrecision) / purchaseTokenPrecision; - } - /*////////////////////////////////////////////////////////////// VIEW FUNCTIONS //////////////////////////////////////////////////////////////*/ diff --git a/src/Strategy.sol b/src/Strategy.sol index 57e41094..8741c3d4 100644 --- a/src/Strategy.sol +++ b/src/Strategy.sol @@ -53,6 +53,7 @@ contract Strategy is BaseStrategy, Pausable, ReentrancyGuard { IERC4626 public immutable YEARN_VAULT; /// @notice State variables + bool public depositLock; /// @dev Previous term controller ITermController public prevTermController; /// @dev Current term controller @@ -62,10 +63,9 @@ contract Strategy is BaseStrategy, Pausable, ReentrancyGuard { TermAuctionListData internal termAuctionListData; uint256 public timeToMaturityThreshold; // seconds uint256 public requiredReserveRatio; // 1e18 - uint256 public discountRateMarkup; // 1e18 (TODO: check this) + uint256 public discountRateMarkup; // 1e18 uint256 public repoTokenConcentrationLimit; // 1e18 mapping(address => bool) public repoTokenBlacklist; - bool public depositLock; modifier notBlacklisted(address repoToken) { if (repoTokenBlacklist[repoToken]) { @@ -344,7 +344,7 @@ contract Strategy is BaseStrategy, Pausable, ReentrancyGuard { uint256 proceeds; if (repoToken != address(0)) { if (!_isTermDeployed(repoToken)) { - revert RepoTokenList.InvalidRepoToken(address(repoToken)); + revert RepoTokenList.InvalidRepoToken(repoToken); } uint256 redemptionTimestamp = repoTokenListData.validateRepoToken( @@ -743,7 +743,7 @@ contract Strategy is BaseStrategy, Pausable, ReentrancyGuard { revert InvalidTermAuction(address(termAuction)); } if (!_isTermDeployed(repoToken)) { - revert RepoTokenList.InvalidRepoToken(address(repoToken)); + revert RepoTokenList.InvalidRepoToken(repoToken); } require(termAuction.termRepoId() == ITermRepoToken(repoToken).termRepoId(), "repoToken does not match term repo ID"); @@ -976,7 +976,7 @@ contract Strategy is BaseStrategy, Pausable, ReentrancyGuard { } /** - * @notice Close the auction + * @notice Required for post-processing after auction clos */ function auctionClosed() external { _sweepAsset(); @@ -1001,7 +1001,7 @@ contract Strategy is BaseStrategy, Pausable, ReentrancyGuard { // Make sure repo token is valid and deployed by Term if (!_isTermDeployed(repoToken)) { - revert RepoTokenList.InvalidRepoToken(address(repoToken)); + revert RepoTokenList.InvalidRepoToken(repoToken); } // Validate and insert the repoToken into the list, retrieve auction rate and redemption timestamp @@ -1101,6 +1101,11 @@ contract Strategy is BaseStrategy, Pausable, ReentrancyGuard { discountRateAdapter = ITermDiscountRateAdapter(_discountRateAdapter); IERC20(_asset).safeApprove(_yearnVault, type(uint256).max); + + timeToMaturityThreshold = 45 days; + requiredReserveRatio = 0.2e18; + discountRateMarkup = 0.005e18; + repoTokenConcentrationLimit = 0.1e18; } /*////////////////////////////////////////////////////////////// @@ -1213,38 +1218,6 @@ contract Strategy is BaseStrategy, Pausable, ReentrancyGuard { return _totalLiquidBalance(); } - /** - * @notice Gets the max amount of `asset` that an address can deposit. - * @dev Defaults to an unlimited amount for any address. But can - * be overridden by strategists. - * - * This function will be called before any deposit or mints to enforce - * any limits desired by the strategist. This can be used for either a - * traditional deposit limit or for implementing a whitelist etc. - * - * EX: - * if(isAllowed[_owner]) return super.availableDepositLimit(_owner); - * - * This does not need to take into account any conversion rates - * from shares to assets. But should know that any non max uint256 - * amounts may be converted to shares. So it is recommended to keep - * custom amounts low enough as not to cause overflow when multiplied - * by `totalSupply`. - * - * @param . The address that is depositing into the strategy. - * @return . The available amount the `_owner` can deposit in terms of `asset` - * - function availableDepositLimit( - address _owner - ) public view override returns (uint256) { - TODO: If desired Implement deposit limit logic and any needed state variables . - - EX: - uint256 totalAssets = TokenizedStrategy.totalAssets(); - return totalAssets >= depositLimit ? 0 : depositLimit - totalAssets; - } - */ - /** * @dev Optional function for strategist to override that can * be called in between reports. @@ -1300,12 +1273,9 @@ contract Strategy is BaseStrategy, Pausable, ReentrancyGuard { * @param _amount The amount of asset to attempt to free. * function _emergencyWithdraw(uint256 _amount) internal override { - TODO: If desired implement simple logic to free deployed funds. - EX: _amount = min(_amount, aToken.balanceOf(address(this))); _freeFunds(_amount); } - */ } diff --git a/src/TermAuctionList.sol b/src/TermAuctionList.sol index 672b3b75..17564a41 100644 --- a/src/TermAuctionList.sol +++ b/src/TermAuctionList.sol @@ -210,7 +210,7 @@ library TermAuctionList { removeNode = true; bytes32[] memory offerIds = new bytes32[](1); offerIds[0] = current; - offer.offerLocker.unlockOffers(offerIds); // unlocking offer in this scenario withdraws offer ammount + offer.offerLocker.unlockOffers(offerIds); // unlocking offer in this scenario withdraws offer amount } } @@ -226,7 +226,6 @@ library TermAuctionList { } if (insertRepoToken) { - // TODO: do we need to validate termDeployed(repoToken) here? // Auction still open => include offerAmount in totalValue // (otherwise locked purchaseToken will be missing from TV) @@ -286,7 +285,7 @@ library TermAuctionList { // 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 /// checking repoTokendiscountRates to make sure we are not double counting on re-openings - if (offer.termAuction.auctionCompleted() && repoTokenListData.discountRates[offer.repoToken] == 0) { + if (repoTokenListData.discountRates[offer.repoToken] == 0 && offer.termAuction.auctionCompleted()) { if (!offer.isRepoTokenSeen) { uint256 repoTokenAmountInBaseAssetPrecision = RepoTokenUtils.getNormalizedRepoTokenAmount( offer.repoToken, @@ -355,7 +354,7 @@ library TermAuctionList { // Handle new repo tokens or reopening auctions /// @dev offer processed, but auctionClosed not yet called and auction is new so repoToken not on List and wont be picked up /// checking repoTokendiscountRates to make sure we are not double counting on re-openings - if (offer.termAuction.auctionCompleted() && repoTokenListData.discountRates[offer.repoToken] == 0) { + if (repoTokenListData.discountRates[offer.repoToken] == 0 && offer.termAuction.auctionCompleted()) { // use normalized repoToken amount if repoToken is not in the list if (!offer.isRepoTokenSeen) { offerAmount = RepoTokenUtils.getNormalizedRepoTokenAmount( diff --git a/src/TermVaultEventEmitter.sol b/src/TermVaultEventEmitter.sol index 84240332..d5716c36 100644 --- a/src/TermVaultEventEmitter.sol +++ b/src/TermVaultEventEmitter.sol @@ -33,6 +33,7 @@ contract TermVaultEventEmitter is Initializable, UUPSUpgradeable, AccessControlU function pairVaultContract(address vaultContract) external onlyRole(ADMIN_ROLE){ _grantRole(VAULT_CONTRACT, vaultContract); + emit VaultContractPaired(vaultContract); } function emitTermControllerUpdated(address oldController, address newController) external onlyRole(VAULT_CONTRACT) { diff --git a/src/interfaces/term/ITermVaultEvents.sol b/src/interfaces/term/ITermVaultEvents.sol index 5ebb1076..c7ec9e79 100644 --- a/src/interfaces/term/ITermVaultEvents.sol +++ b/src/interfaces/term/ITermVaultEvents.sol @@ -2,6 +2,8 @@ pragma solidity ^0.8.18; interface ITermVaultEvents { + event VaultContractPaired(address vault); + event TermControllerUpdated(address oldController, address newController); event TimeToMaturityThresholdUpdated(uint256 oldThreshold, uint256 newThreshold); diff --git a/src/periphery/StrategyAprOracle.sol b/src/periphery/StrategyAprOracle.sol index 1c0bfb8f..96d0f8ab 100644 --- a/src/periphery/StrategyAprOracle.sol +++ b/src/periphery/StrategyAprOracle.sol @@ -28,7 +28,7 @@ contract StrategyAprOracle is AprOracleBase { function aprAfterDebtChange( address _strategy, int256 _delta - ) external view override returns (uint256) { + ) external pure override returns (uint256) { // TODO: Implement any necessary logic to return the most accurate // APR estimation for the strategy. return 1e17; From eca56c444d732f3222b7e7c1cb85886bf81fd5b1 Mon Sep 17 00:00:00 2001 From: Andrew Zhou Date: Mon, 9 Sep 2024 19:06:59 -0700 Subject: [PATCH 06/20] cache total asset value --- src/Strategy.sol | 23 ++++++++++++++++------- 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/src/Strategy.sol b/src/Strategy.sol index 57e41094..3c0c6645 100644 --- a/src/Strategy.sol +++ b/src/Strategy.sol @@ -545,7 +545,12 @@ contract Strategy is BaseStrategy, Pausable, ReentrancyGuard { * @param liquidBalanceToRemove The amount of liquid balance to be removed from the strategy * * @dev This function calculates the concentration ratio of the specified repoToken - * and compares it against the predefined concentration limit. It reverts with a + * and compares it against the simulatedRepoTokenConcentrationRatio = _getRepoTokenConcentrationRatio( + repoToken, + repoTokenAmountInBaseAssetPrecision, + _totalAssetValue(liquidBalance), + proceeds + );predefined concentration limit. It reverts with a * RepoTokenConcentrationTooHigh error if the concentration exceeds the limit. */ function _validateRepoTokenConcentration( @@ -830,9 +835,11 @@ contract Strategy is BaseStrategy, Pausable, ReentrancyGuard { // Retrieve the total liquid balance uint256 liquidBalance = _totalLiquidBalance(); + uint256 totalAssetValue = _totalAssetValue(liquidBalance); + uint256 liquidReserveRatio = totalAssetValue == 0 ? 0 : liquidBalance * 1e18 / totalAssetValue; // Check that new offer does not violate reserve ratio constraint - if (_liquidReserveRatio(liquidBalance) < requiredReserveRatio) { + if (liquidReserveRatio < requiredReserveRatio) { revert BalanceBelowRequiredReserveRatio(); } @@ -850,7 +857,7 @@ contract Strategy is BaseStrategy, Pausable, ReentrancyGuard { } // Passing in 0 amount and 0 liquid balance adjustment because offer and balance already updated - _validateRepoTokenConcentration(repoToken, 0, _totalAssetValue(liquidBalance), 0); + _validateRepoTokenConcentration(repoToken, 0, totalAssetValue, 0); } /** @@ -1016,9 +1023,11 @@ contract Strategy is BaseStrategy, Pausable, ReentrancyGuard { _sweepAsset(); _redeemRepoTokens(0); - // Retrieve total liquid balance and ensure it's greater than zero + // Retrieve total asset value and liquid balance and ensure they are greater than zero uint256 liquidBalance = _totalLiquidBalance(); require(liquidBalance > 0); + uint256 totalAssetValue = _totalAssetValue(liquidBalance); + require(totalAssetValue > 0); // Calculate the repoToken amount in base asset precision uint256 repoTokenPrecision = 10 ** ERC20(repoToken).decimals(); @@ -1052,8 +1061,8 @@ contract Strategy is BaseStrategy, Pausable, ReentrancyGuard { } // Ensure the remaining liquid balance is above the liquidity threshold - uint256 newLiquidBalance = liquidBalance - proceeds; - if (_liquidReserveRatio(newLiquidBalance) < requiredReserveRatio) { + uint256 newLiquidReserveRatio = ( liquidBalance - proceeds ) * 1e18 / totalAssetValue; + if (newLiquidReserveRatio < requiredReserveRatio) { revert BalanceBelowRequiredReserveRatio(); } @@ -1061,7 +1070,7 @@ contract Strategy is BaseStrategy, Pausable, ReentrancyGuard { _validateRepoTokenConcentration( repoToken, repoTokenAmountInBaseAssetPrecision, - _totalAssetValue(liquidBalance), + totalAssetValue, proceeds ); From 9316f4debaea2357aa48bc330b24d816b3d5bcf1 Mon Sep 17 00:00:00 2001 From: Andrew Zhou Date: Mon, 9 Sep 2024 19:08:41 -0700 Subject: [PATCH 07/20] docs error --- src/Strategy.sol | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/src/Strategy.sol b/src/Strategy.sol index 3c0c6645..d3128228 100644 --- a/src/Strategy.sol +++ b/src/Strategy.sol @@ -545,12 +545,7 @@ contract Strategy is BaseStrategy, Pausable, ReentrancyGuard { * @param liquidBalanceToRemove The amount of liquid balance to be removed from the strategy * * @dev This function calculates the concentration ratio of the specified repoToken - * and compares it against the simulatedRepoTokenConcentrationRatio = _getRepoTokenConcentrationRatio( - repoToken, - repoTokenAmountInBaseAssetPrecision, - _totalAssetValue(liquidBalance), - proceeds - );predefined concentration limit. It reverts with a + * and compares it against the predefined concentration limit. It reverts with a * RepoTokenConcentrationTooHigh error if the concentration exceeds the limit. */ function _validateRepoTokenConcentration( From 6c266ac4bda5bb9a7dcda8d65ca19820489fc7f3 Mon Sep 17 00:00:00 2001 From: Andrew Zhou Date: Mon, 9 Sep 2024 19:10:43 -0700 Subject: [PATCH 08/20] require total asset value > 0 in submitAuctionOffer --- src/Strategy.sol | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/Strategy.sol b/src/Strategy.sol index d3128228..429f9262 100644 --- a/src/Strategy.sol +++ b/src/Strategy.sol @@ -831,7 +831,8 @@ contract Strategy is BaseStrategy, Pausable, ReentrancyGuard { // Retrieve the total liquid balance uint256 liquidBalance = _totalLiquidBalance(); uint256 totalAssetValue = _totalAssetValue(liquidBalance); - uint256 liquidReserveRatio = totalAssetValue == 0 ? 0 : liquidBalance * 1e18 / totalAssetValue; + require(totalAssetValue > 0); + uint256 liquidReserveRatio = liquidBalance * 1e18 / totalAssetValue; // NOTE: we require totalAssetValue > 0 above // Check that new offer does not violate reserve ratio constraint if (liquidReserveRatio < requiredReserveRatio) { @@ -1056,7 +1057,7 @@ contract Strategy is BaseStrategy, Pausable, ReentrancyGuard { } // Ensure the remaining liquid balance is above the liquidity threshold - uint256 newLiquidReserveRatio = ( liquidBalance - proceeds ) * 1e18 / totalAssetValue; + uint256 newLiquidReserveRatio = ( liquidBalance - proceeds ) * 1e18 / totalAssetValue; // note we require totalAssetValue > 0 above if (newLiquidReserveRatio < requiredReserveRatio) { revert BalanceBelowRequiredReserveRatio(); } From 90298bf84bd9b173eeb5dbfc1c629a7315d0b808 Mon Sep 17 00:00:00 2001 From: Andrew Zhou Date: Mon, 9 Sep 2024 19:11:42 -0700 Subject: [PATCH 09/20] align comments --- src/Strategy.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Strategy.sol b/src/Strategy.sol index 429f9262..71fc9c00 100644 --- a/src/Strategy.sol +++ b/src/Strategy.sol @@ -1057,7 +1057,7 @@ contract Strategy is BaseStrategy, Pausable, ReentrancyGuard { } // Ensure the remaining liquid balance is above the liquidity threshold - uint256 newLiquidReserveRatio = ( liquidBalance - proceeds ) * 1e18 / totalAssetValue; // note we require totalAssetValue > 0 above + uint256 newLiquidReserveRatio = ( liquidBalance - proceeds ) * 1e18 / totalAssetValue; // NOTE: we require totalAssetValue > 0 above if (newLiquidReserveRatio < requiredReserveRatio) { revert BalanceBelowRequiredReserveRatio(); } From ce3758d37741db9d3facc5a6676efc2204f8ef30 Mon Sep 17 00:00:00 2001 From: Andrew Zhou Date: Mon, 9 Sep 2024 19:19:19 -0700 Subject: [PATCH 10/20] sweep if funds freed --- src/Strategy.sol | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/Strategy.sol b/src/Strategy.sol index 57e41094..67cbcb24 100644 --- a/src/Strategy.sol +++ b/src/Strategy.sol @@ -901,7 +901,7 @@ contract Strategy is BaseStrategy, Pausable, ReentrancyGuard { address(repoServicer.termRepoLocker()), offerDebit ); - } + } // Submit the offer and get the offer IDs offerIds = offerLocker.lockOffers(offerSubmissions); @@ -925,6 +925,11 @@ contract Strategy is BaseStrategy, Pausable, ReentrancyGuard { PendingOffer storage pendingOffer = termAuctionListData.offers[offerIds[0]]; pendingOffer.offerAmount = offer.amount; } + + if (newOfferAmount < currentOfferAmount) { + YEARN_VAULT.deposit(IERC20(asset).balanceOf(address(this)), address(this)); + } + } /** From 0cc51aaa3ef66ac5dd68476584d58dedecc3617f Mon Sep 17 00:00:00 2001 From: Andrew Zhou Date: Mon, 9 Sep 2024 19:20:17 -0700 Subject: [PATCH 11/20] remove extra space --- src/Strategy.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Strategy.sol b/src/Strategy.sol index 67cbcb24..014ef75f 100644 --- a/src/Strategy.sol +++ b/src/Strategy.sol @@ -901,7 +901,7 @@ contract Strategy is BaseStrategy, Pausable, ReentrancyGuard { address(repoServicer.termRepoLocker()), offerDebit ); - } + } // Submit the offer and get the offer IDs offerIds = offerLocker.lockOffers(offerSubmissions); From a6410f213df636a57bad17e8c413e9ff20e476ea Mon Sep 17 00:00:00 2001 From: Andrew Zhou Date: Tue, 10 Sep 2024 16:08:34 -0700 Subject: [PATCH 12/20] fixes getNormalized repo token amount to only apply redemption haircut if nonzero --- src/RepoTokenUtils.sol | 4 +++- src/test/mocks/MockTermAuctionOfferLocker.sol | 4 ++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/RepoTokenUtils.sol b/src/RepoTokenUtils.sol index ee9dde0f..47fef655 100644 --- a/src/RepoTokenUtils.sol +++ b/src/RepoTokenUtils.sol @@ -56,7 +56,9 @@ library RepoTokenUtils { uint256 repoTokenPrecision = 10**ERC20(repoToken).decimals(); uint256 redemptionValue = ITermRepoToken(repoToken).redemptionValue(); repoTokenAmountInBaseAssetPrecision = + repoRedemptionHaircut != 0 ? (redemptionValue * repoRedemptionHaircut * repoTokenAmount * purchaseTokenPrecision) / - (repoTokenPrecision * RATE_PRECISION * 1e18); + (repoTokenPrecision * RATE_PRECISION * 1e18) + : (redemptionValue * repoTokenAmount * purchaseTokenPrecision) / (repoTokenPrecision * RATE_PRECISION); } } \ No newline at end of file diff --git a/src/test/mocks/MockTermAuctionOfferLocker.sol b/src/test/mocks/MockTermAuctionOfferLocker.sol index 2c69173d..7748ea37 100644 --- a/src/test/mocks/MockTermAuctionOfferLocker.sol +++ b/src/test/mocks/MockTermAuctionOfferLocker.sol @@ -25,7 +25,7 @@ contract MockTermAuctionOfferLocker is ITermAuctionOfferLocker { purchaseToken = _purchaseToken; termRepoServicer = _repoServicer; repoLocker = MockTermRepoLocker(_repoLocker); - auctionStartTime = block.timestamp; + auctionStartTime = 0; } function termRepoId() external view returns (bytes32) { @@ -41,7 +41,7 @@ contract MockTermAuctionOfferLocker is ITermAuctionOfferLocker { } function revealTime() external view returns (uint256) { - + return auction.auctionEndTime(); } function lockedOffer(bytes32 id) external view returns (TermAuctionOffer memory) { From a85d28668996d2e7b632dc4becad5cb67bcdf009 Mon Sep 17 00:00:00 2001 From: 0xddong Date: Tue, 10 Sep 2024 23:00:29 -0700 Subject: [PATCH 13/20] fixing unit tests --- src/test/TestUSDCOffers.t.sol | 2 ++ src/test/TestUSDCSellRepoToken.t.sol | 2 ++ src/test/TestUSDCSubmitOffer.t.sol | 2 ++ 3 files changed, 6 insertions(+) diff --git a/src/test/TestUSDCOffers.t.sol b/src/test/TestUSDCOffers.t.sol index 5dfccf4a..ea30441a 100644 --- a/src/test/TestUSDCOffers.t.sol +++ b/src/test/TestUSDCOffers.t.sol @@ -37,6 +37,8 @@ contract TestUSDCSubmitOffer is Setup { termStrategy.setCollateralTokenParams(address(mockCollateral), 0.5e18); termStrategy.setTimeToMaturityThreshold(3 weeks); termStrategy.setRepoTokenConcentrationLimit(1e18); + termStrategy.setRequiredReserveRatio(0); + termStrategy.setdiscountRateMarkup(0); vm.stopPrank(); // start with some initial funds diff --git a/src/test/TestUSDCSellRepoToken.t.sol b/src/test/TestUSDCSellRepoToken.t.sol index 1181d154..c6c6ee23 100644 --- a/src/test/TestUSDCSellRepoToken.t.sol +++ b/src/test/TestUSDCSellRepoToken.t.sol @@ -49,6 +49,8 @@ contract TestUSDCSellRepoToken is Setup { termStrategy.setCollateralTokenParams(address(mockCollateral), 0.5e18); termStrategy.setTimeToMaturityThreshold(10 weeks); termStrategy.setRepoTokenConcentrationLimit(1e18); + termStrategy.setRequiredReserveRatio(0); + termStrategy.setdiscountRateMarkup(0); vm.stopPrank(); } diff --git a/src/test/TestUSDCSubmitOffer.t.sol b/src/test/TestUSDCSubmitOffer.t.sol index 0550c844..07bffcca 100644 --- a/src/test/TestUSDCSubmitOffer.t.sol +++ b/src/test/TestUSDCSubmitOffer.t.sol @@ -37,6 +37,8 @@ contract TestUSDCSubmitOffer is Setup { termStrategy.setCollateralTokenParams(address(mockCollateral), 0.5e18); termStrategy.setTimeToMaturityThreshold(3 weeks); termStrategy.setRepoTokenConcentrationLimit(1e18); + termStrategy.setRequiredReserveRatio(0); + termStrategy.setdiscountRateMarkup(0); vm.stopPrank(); // start with some initial funds From ea792f153178d72e0d0d97cab65b606d10801f33 Mon Sep 17 00:00:00 2001 From: Andrew Zhou Date: Wed, 11 Sep 2024 17:07:52 -0700 Subject: [PATCH 14/20] get rid of unneccessary looping for present value of single token --- src/RepoTokenList.sol | 26 ++------------------------ src/Strategy.sol | 19 +++++++++++-------- 2 files changed, 13 insertions(+), 32 deletions(-) diff --git a/src/RepoTokenList.sol b/src/RepoTokenList.sol index 1c586604..31029176 100644 --- a/src/RepoTokenList.sol +++ b/src/RepoTokenList.sol @@ -175,35 +175,19 @@ library RepoTokenList { * @param listData The list data * @param discountRateAdapter The discount rate adapter * @param purchaseTokenPrecision The precision of the purchase token - * @param repoTokenToMatch The address of the repoToken to match (optional) * @return totalPresentValue The total present value of the repoTokens - * @dev If the `repoTokenToMatch` parameter is provided (non-zero address), the function will filter - * the calculations to include only the specified repoToken. If `repoTokenToMatch` is not provided - * (zero address), it will aggregate the present value of all repoTokens in the list. - * - * Example usage: - * - To get the present value of all repoTokens: call with `repoTokenToMatch` set to `address(0)`. - * - To get the present value of a specific repoToken: call with `repoTokenToMatch` set to the address of the desired repoToken. + * @dev Aaggregates the present value of all repoTokens in the list. */ function getPresentValue( RepoTokenListData storage listData, ITermDiscountRateAdapter discountRateAdapter, - uint256 purchaseTokenPrecision, - address repoTokenToMatch + uint256 purchaseTokenPrecision ) 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 - // 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 discountRate = discountRateAdapter.getDiscountRate(current); @@ -224,12 +208,6 @@ library RepoTokenList { 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); } diff --git a/src/Strategy.sol b/src/Strategy.sol index fd7ccf78..2607f6a4 100644 --- a/src/Strategy.sol +++ b/src/Strategy.sol @@ -428,12 +428,16 @@ contract Strategy is BaseStrategy, Pausable, ReentrancyGuard { function getRepoTokenHoldingValue( address repoToken ) public view returns (uint256) { - return - repoTokenListData.getPresentValue( - discountRateAdapter, - PURCHASE_TOKEN_PRECISION, - repoToken - ) + + uint256 repoTokenHoldingPV; + if (repoTokenListData.discountRates[repoToken] != 0) { + repoTokenHoldingPV = calculateRepoTokenPresentValue( + repoToken, + discountRateAdapter.getDiscountRate(repoToken), + ITermRepoToken(repoToken).balanceOf(address(this)) + ); + } + return + repoTokenHoldingPV + termAuctionListData.getPresentValue( repoTokenListData, discountRateAdapter, @@ -491,8 +495,7 @@ contract Strategy is BaseStrategy, Pausable, ReentrancyGuard { liquidBalance + repoTokenListData.getPresentValue( discountRateAdapter, - PURCHASE_TOKEN_PRECISION, - address(0) + PURCHASE_TOKEN_PRECISION ) + termAuctionListData.getPresentValue( repoTokenListData, From 706bef2228c72d50f1fc1a863a298374bbb1bf10 Mon Sep 17 00:00:00 2001 From: Andrew Zhou Date: Wed, 11 Sep 2024 18:48:45 -0700 Subject: [PATCH 15/20] remove memory load --- src/TermAuctionList.sol | 138 +++++++++++++---------------- src/test/TestUSDCSubmitOffer.t.sol | 2 +- 2 files changed, 61 insertions(+), 79 deletions(-) diff --git a/src/TermAuctionList.sol b/src/TermAuctionList.sol index 7a4554b8..4822fe77 100644 --- a/src/TermAuctionList.sol +++ b/src/TermAuctionList.sol @@ -16,16 +16,6 @@ struct PendingOffer { ITermAuctionOfferLocker offerLocker; } -// In-memory representation of an offer object -struct PendingOfferMemory { - bytes32 offerId; - address repoToken; - uint256 offerAmount; - ITermAuction termAuction; - ITermAuctionOfferLocker offerLocker; - bool isRepoTokenSeen; -} - struct TermAuctionListNode { bytes32 next; } @@ -59,51 +49,6 @@ library TermAuctionList { return listData.nodes[current].next; } - /** - * @notice Loads all pending offers into an array of `PendingOfferMemory` structs - * @param listData The list data - * @return offers An array of structs containing details of all pending offers - * - * @dev This function iterates through the list of offers and gathers their details into an array of `PendingOfferMemory` structs. - * This makes it easier to process and analyze the pending offers. - */ - function _loadOffers(TermAuctionListData storage listData) private view returns (PendingOfferMemory[] memory offers) { - uint256 len = _count(listData); - offers = new PendingOfferMemory[](len); - - uint256 i; - bytes32 current = listData.head; - while (current != NULL_NODE) { - PendingOffer memory currentOffer = listData.offers[current]; - PendingOfferMemory memory newOffer = offers[i]; - - newOffer.offerId = current; - newOffer.repoToken = currentOffer.repoToken; - newOffer.offerAmount = currentOffer.offerAmount; - newOffer.termAuction = currentOffer.termAuction; - newOffer.offerLocker = currentOffer.offerLocker; - - i++; - current = _getNext(listData, current); - } - } - - /** - * @notice Marks a specific repoToken as seen within an array of `PendingOfferMemory` structs - * @param offers The array of `PendingOfferMemory` structs representing the pending offers - * @param repoToken The address of the repoToken to be marked as seen - * - * @dev This function iterates through the `offers` array and sets the `isRepoTokenSeen` flag to `true` - * for the specified `repoToken`. This helps to avoid double-counting or reprocessing the same repoToken. - */ - function _markRepoTokenAsSeen(PendingOfferMemory[] memory offers, address repoToken) private pure { - for (uint256 i; i < offers.length; i++) { - if (repoToken == offers[i].repoToken) { - offers[i].isRepoTokenSeen = true; - } - } - } - /*////////////////////////////////////////////////////////////// INTERNAL FUNCTIONS //////////////////////////////////////////////////////////////*/ @@ -149,17 +94,52 @@ library TermAuctionList { * @param offerId The ID of the offer to be inserted * @param pendingOffer The `PendingOffer` struct containing details of the offer to be inserted * - * @dev This function inserts a new pending offer at the beginning of the linked list in the `TermAuctionListData` structure. - * It updates the `next` pointers and the head of the list to ensure the new offer is correctly linked. + * @dev This function inserts a new pending offer while maintaining the list sorted by auction address. + * The function iterates through the list to find the correct position for the new `offerId` and updates the pointers accordingly. */ function insertPending(TermAuctionListData storage listData, bytes32 offerId, PendingOffer memory pendingOffer) internal { bytes32 current = listData.head; - if (current != NULL_NODE) { - listData.nodes[offerId].next = current; + // If the list is empty, set the new repoToken as the head + if (current == NULL_NODE) { + listData.head = offerId; + return; } - listData.head = offerId; + bytes32 prev; + while (current != NULL_NODE) { + + // If the offerId is already in the list, exit + if (current == offerId) { + break; + } + + address currentAuction = address(listData.offers[current].termAuction); + address auctionToInsert = address(pendingOffer.termAuction); + + // Insert repoToken before current if its maturity is less than or equal + if (auctionToInsert <= currentAuction) { + if (prev == NULL_NODE) { + listData.head = offerId; + } else { + listData.nodes[prev].next = offerId; + } + listData.nodes[offerId].next = current; + break; + } + + // Move to the next node + bytes32 next = _getNext(listData, current); + + // If at the end of the list, insert repoToken after current + if (next == NULL_NODE) { + listData.nodes[current].next = offerId; + break; + } + + prev = current; + current = next; + } listData.offers[offerId] = pendingOffer; } @@ -267,12 +247,11 @@ library TermAuctionList { ) internal view returns (uint256 totalValue) { // Return 0 if the list is empty if (listData.head == NULL_NODE) return 0; + address edgeCaseAuction; // NOTE: handle edge case, assumes that pendingOffer is properly sorted by auction address - // Load pending offers - PendingOfferMemory[] memory offers = _loadOffers(listData); - - for (uint256 i; i < offers.length; i++) { - PendingOfferMemory memory offer = offers[i]; + bytes32 current = listData.head; + while (current != NULL_NODE) { + PendingOffer storage offer = listData.offers[current]; // Filter by specific repo token if provided, address(0) bypasses this filter if (repoTokenToMatch != address(0) && offer.repoToken != repoTokenToMatch) { @@ -280,13 +259,13 @@ library TermAuctionList { continue; } - uint256 offerAmount = offer.offerLocker.lockedOffer(offer.offerId).amount; + uint256 offerAmount = offer.offerLocker.lockedOffer(current).amount; // 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 /// checking repoTokendiscountRates to make sure we are not double counting on re-openings if (repoTokenListData.discountRates[offer.repoToken] == 0 && offer.termAuction.auctionCompleted()) { - if (!offer.isRepoTokenSeen) { + if (edgeCaseAuction != address(offer.termAuction)) { uint256 repoTokenAmountInBaseAssetPrecision = RepoTokenUtils.getNormalizedRepoTokenAmount( offer.repoToken, ITermRepoToken(offer.repoToken).balanceOf(address(this)), @@ -300,15 +279,18 @@ library TermAuctionList { discountRateAdapter.getDiscountRate(offer.repoToken) ); - // Mark the repo token as seen to avoid double counting - // since multiple offers can be tied to the same repoToken, we need to mark - // the repoTokens we've seen to avoid double counting - _markRepoTokenAsSeen(offers, offer.repoToken); + // Mark the edge case auction as processed to avoid double counting + // since multiple offers can be tied to the same auction, we need to mark + // the edge case auction as processed to avoid double counting + edgeCaseAuction = address(offer.termAuction); } } else { // Add the offer amount to the total value totalValue += offerAmount; } + + // Move to the next token in the list + current = _getNext(listData, current); } } @@ -339,12 +321,12 @@ library TermAuctionList { ) internal view returns (uint256 cumulativeWeightedTimeToMaturity, uint256 cumulativeOfferAmount, bool found) { // If the list is empty, return 0s and false if (listData.head == NULL_NODE) return (0, 0, false); + address edgeCaseAuction; // NOTE: handle edge case, assumes that pendingOffer is properly sorted by auction address - // Load pending offers from the list data - PendingOfferMemory[] memory offers = _loadOffers(listData); - for (uint256 i; i < offers.length; i++) { - PendingOfferMemory memory offer = offers[i]; + bytes32 current = listData.head; + while (current != NULL_NODE) { + PendingOffer storage offer =listData.offers[current]; uint256 offerAmount; if (offer.repoToken == repoToken) { @@ -352,14 +334,14 @@ library TermAuctionList { found = true; } else { // Retrieve the current offer amount from the offer locker - offerAmount = offer.offerLocker.lockedOffer(offer.offerId).amount; + offerAmount = offer.offerLocker.lockedOffer(current).amount; // Handle new repo tokens or reopening auctions /// @dev offer processed, but auctionClosed not yet called and auction is new so repoToken not on List and wont be picked up /// checking repoTokendiscountRates to make sure we are not double counting on re-openings if (repoTokenListData.discountRates[offer.repoToken] == 0 && offer.termAuction.auctionCompleted()) { // use normalized repoToken amount if repoToken is not in the list - if (!offer.isRepoTokenSeen) { + if (edgeCaseAuction != address(offer.termAuction)) { offerAmount = RepoTokenUtils.getNormalizedRepoTokenAmount( offer.repoToken, ITermRepoToken(offer.repoToken).balanceOf(address(this)), @@ -367,7 +349,7 @@ library TermAuctionList { discountRateAdapter.repoRedemptionHaircut(offer.repoToken) ); - _markRepoTokenAsSeen(offers, offer.repoToken); + edgeCaseAuction = address(offer.termAuction); } } } diff --git a/src/test/TestUSDCSubmitOffer.t.sol b/src/test/TestUSDCSubmitOffer.t.sol index 07bffcca..5abd318c 100644 --- a/src/test/TestUSDCSubmitOffer.t.sol +++ b/src/test/TestUSDCSubmitOffer.t.sol @@ -8,7 +8,7 @@ import {MockUSDC} from "./mocks/MockUSDC.sol"; import {Setup, ERC20, IStrategyInterface} from "./utils/Setup.sol"; import {Strategy} from "../Strategy.sol"; -contract TestUSDCSubmitOffer is Setup { +contract TestUSDCSubmitOf1er1 is Setup { uint256 internal constant TEST_REPO_TOKEN_RATE = 0.05e18; MockUSDC internal mockUSDC; From 3c59187910b456250984ebb89a74a2ebd0cff4f5 Mon Sep 17 00:00:00 2001 From: Andrew Zhou Date: Wed, 11 Sep 2024 19:20:15 -0700 Subject: [PATCH 16/20] fix test name --- src/test/TestUSDCSubmitOffer.t.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/TestUSDCSubmitOffer.t.sol b/src/test/TestUSDCSubmitOffer.t.sol index 5abd318c..07bffcca 100644 --- a/src/test/TestUSDCSubmitOffer.t.sol +++ b/src/test/TestUSDCSubmitOffer.t.sol @@ -8,7 +8,7 @@ import {MockUSDC} from "./mocks/MockUSDC.sol"; import {Setup, ERC20, IStrategyInterface} from "./utils/Setup.sol"; import {Strategy} from "../Strategy.sol"; -contract TestUSDCSubmitOf1er1 is Setup { +contract TestUSDCSubmitOffer is Setup { uint256 internal constant TEST_REPO_TOKEN_RATE = 0.05e18; MockUSDC internal mockUSDC; From 7ab2aaaf013d9aecad03e2bcfb2d2682aa28caaf Mon Sep 17 00:00:00 2001 From: Andrew Zhou Date: Wed, 11 Sep 2024 19:24:48 -0700 Subject: [PATCH 17/20] increment linked list counter --- src/TermAuctionList.sol | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/TermAuctionList.sol b/src/TermAuctionList.sol index 4822fe77..2b11cd9b 100644 --- a/src/TermAuctionList.sol +++ b/src/TermAuctionList.sol @@ -363,6 +363,8 @@ library TermAuctionList { cumulativeWeightedTimeToMaturity += weightedTimeToMaturity; cumulativeOfferAmount += offerAmount; } + // Move to the next token in the list + current = _getNext(listData, current); } } } \ No newline at end of file From 6d1ea56bba907f6114c48446b116f7083ace9c9f Mon Sep 17 00:00:00 2001 From: Andrew Zhou Date: Wed, 11 Sep 2024 20:40:32 -0700 Subject: [PATCH 18/20] load offer into memory on first insert in auction list --- src/TermAuctionList.sol | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/TermAuctionList.sol b/src/TermAuctionList.sol index 2b11cd9b..b3d623f3 100644 --- a/src/TermAuctionList.sol +++ b/src/TermAuctionList.sol @@ -8,6 +8,9 @@ import {ITermDiscountRateAdapter} from "./interfaces/term/ITermDiscountRateAdapt import {RepoTokenList, RepoTokenListData} from "./RepoTokenList.sol"; import {RepoTokenUtils} from "./RepoTokenUtils.sol"; +import "forge-std/console.sol"; + + // In-storage representation of an offer object struct PendingOffer { address repoToken; @@ -103,6 +106,7 @@ library TermAuctionList { // If the list is empty, set the new repoToken as the head if (current == NULL_NODE) { listData.head = offerId; + listData.offers[offerId] = pendingOffer; return; } @@ -363,6 +367,7 @@ library TermAuctionList { cumulativeWeightedTimeToMaturity += weightedTimeToMaturity; cumulativeOfferAmount += offerAmount; } + // Move to the next token in the list current = _getNext(listData, current); } From a23c0d987a6f73ecbfc174069be6b38dbfd9d52d Mon Sep 17 00:00:00 2001 From: Andrew Zhou Date: Wed, 11 Sep 2024 20:42:26 -0700 Subject: [PATCH 19/20] cleanup --- src/TermAuctionList.sol | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/TermAuctionList.sol b/src/TermAuctionList.sol index b3d623f3..c3d0f9ca 100644 --- a/src/TermAuctionList.sol +++ b/src/TermAuctionList.sol @@ -8,9 +8,6 @@ import {ITermDiscountRateAdapter} from "./interfaces/term/ITermDiscountRateAdapt import {RepoTokenList, RepoTokenListData} from "./RepoTokenList.sol"; import {RepoTokenUtils} from "./RepoTokenUtils.sol"; -import "forge-std/console.sol"; - - // In-storage representation of an offer object struct PendingOffer { address repoToken; @@ -353,6 +350,10 @@ library TermAuctionList { discountRateAdapter.repoRedemptionHaircut(offer.repoToken) ); + + // Mark the edge case auction as processed to avoid double counting + // since multiple offers can be tied to the same auction, we need to mark + // the edge case auction as processed to avoid double counting edgeCaseAuction = address(offer.termAuction); } } From c36a2be1e798ffb90bf111f891aea196448a1f98 Mon Sep 17 00:00:00 2001 From: Andrew Zhou Date: Thu, 12 Sep 2024 11:35:39 -0700 Subject: [PATCH 20/20] fix mispelling in docs --- src/RepoTokenList.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/RepoTokenList.sol b/src/RepoTokenList.sol index 31029176..391eb224 100644 --- a/src/RepoTokenList.sol +++ b/src/RepoTokenList.sol @@ -176,7 +176,7 @@ library RepoTokenList { * @param discountRateAdapter The discount rate adapter * @param purchaseTokenPrecision The precision of the purchase token * @return totalPresentValue The total present value of the repoTokens - * @dev Aaggregates the present value of all repoTokens in the list. + * @dev Aggregates the present value of all repoTokens in the list. */ function getPresentValue( RepoTokenListData storage listData,