From 9ff455c78cfc53ff4cc0a2377c0c3752c308765e Mon Sep 17 00:00:00 2001 From: 0xddong Date: Thu, 25 Jul 2024 14:00:07 -0700 Subject: [PATCH] discount rate adapter --- src/RepoTokenList.sol | 18 ++++----------- src/Strategy.sol | 19 +++++++++++---- src/TermAuctionList.sol | 10 +++++--- src/TermDiscountRateAdapter.sol | 23 +++++++++++++++++++ src/TermVaultEventEmitter.sol | 7 ++++++ .../term/ITermDiscountRateAdapter.sol | 6 +++++ src/interfaces/term/ITermVaultEvents.sol | 10 ++++++++ src/test/TestUSDCSellRepoToken.t.sol | 2 +- src/test/utils/Setup.sol | 11 ++++++++- 9 files changed, 83 insertions(+), 23 deletions(-) create mode 100644 src/TermDiscountRateAdapter.sol create mode 100644 src/interfaces/term/ITermDiscountRateAdapter.sol diff --git a/src/RepoTokenList.sol b/src/RepoTokenList.sol index 53b59c76..371dc069 100644 --- a/src/RepoTokenList.sol +++ b/src/RepoTokenList.sol @@ -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"; @@ -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, @@ -212,6 +201,7 @@ library RepoTokenList { RepoTokenListData storage listData, ITermRepoToken repoToken, ITermController termController, + ITermDiscountRateAdapter discountRateAdapter, address asset ) internal returns (uint256 auctionRate, uint256 redemptionTimestamp) { auctionRate = listData.auctionRates[address(repoToken)]; @@ -223,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); diff --git a/src/Strategy.sol b/src/Strategy.sol index 97775e6c..7138e69f 100644 --- a/src/Strategy.sol +++ b/src/Strategy.sol @@ -12,6 +12,7 @@ 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"; @@ -46,6 +47,7 @@ contract Strategy is BaseStrategy, Pausable, ReentrancyGuard { IERC4626 public immutable YEARN_VAULT; ITermController public termController; + ITermDiscountRateAdapter public discountRateAdapter; RepoTokenListData internal repoTokenListData; TermAuctionListData internal termAuctionListData; uint256 public timeToMaturityThreshold; // seconds @@ -103,6 +105,11 @@ contract Strategy is BaseStrategy, Pausable, ReentrancyGuard { 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(); } @@ -203,7 +210,7 @@ contract Strategy is BaseStrategy, Pausable, ReentrancyGuard { } 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)); @@ -232,6 +239,7 @@ contract Strategy is BaseStrategy, Pausable, ReentrancyGuard { (uint256 auctionRate, uint256 redemptionTimestamp) = repoTokenListData.validateAndInsertRepoToken( ITermRepoToken(repoToken), termController, + discountRateAdapter, address(asset) ); @@ -306,7 +314,7 @@ contract Strategy is BaseStrategy, Pausable, ReentrancyGuard { offerLocker.unlockOffers(offerIds); - termAuctionListData.removeCompleted(repoTokenListData, termController, address(asset)); + termAuctionListData.removeCompleted(repoTokenListData, termController, discountRateAdapter, address(asset)); _sweepAssetAndRedeemRepoTokens(0); } @@ -483,7 +491,7 @@ contract Strategy is BaseStrategy, Pausable, ReentrancyGuard { function getRepoTokenValue(address repoToken) public view returns (uint256) { return repoTokenListData.getPresentValue(PURCHASE_TOKEN_PRECISION, repoToken) + termAuctionListData.getPresentValue( - repoTokenListData, termController, PURCHASE_TOKEN_PRECISION, repoToken + repoTokenListData, discountRateAdapter, PURCHASE_TOKEN_PRECISION, repoToken ); } @@ -491,7 +499,7 @@ contract Strategy is BaseStrategy, Pausable, ReentrancyGuard { return _totalLiquidBalance(address(this)) + repoTokenListData.getPresentValue(PURCHASE_TOKEN_PRECISION, address(0)) + termAuctionListData.getPresentValue( - repoTokenListData, termController, PURCHASE_TOKEN_PRECISION, address(0) + repoTokenListData, discountRateAdapter, PURCHASE_TOKEN_PRECISION, address(0) ); } @@ -499,12 +507,15 @@ contract Strategy is BaseStrategy, Pausable, ReentrancyGuard { 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); } diff --git a/src/TermAuctionList.sol b/src/TermAuctionList.sol index 8f9e4429..e29c75dc 100644 --- a/src/TermAuctionList.sol +++ b/src/TermAuctionList.sol @@ -5,6 +5,7 @@ import {ITermAuction} from "./interfaces/term/ITermAuction.sol"; import {ITermAuctionOfferLocker} from "./interfaces/term/ITermAuctionOfferLocker.sol"; import {ITermController} from "./interfaces/term/ITermController.sol"; import {ITermRepoToken} from "./interfaces/term/ITermRepoToken.sol"; +import {ITermDiscountRateAdapter} from "./interfaces/term/ITermDiscountRateAdapter.sol"; import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; import {RepoTokenList, RepoTokenListData} from "./RepoTokenList.sol"; import {RepoTokenUtils} from "./RepoTokenUtils.sol"; @@ -81,6 +82,7 @@ library TermAuctionList { TermAuctionListData storage listData, RepoTokenListData storage repoTokenListData, ITermController termController, + ITermDiscountRateAdapter discountRateAdapter, address asset ) internal { /* @@ -134,7 +136,9 @@ library TermAuctionList { } if (insertRepoToken) { - repoTokenListData.validateAndInsertRepoToken(ITermRepoToken(offer.repoToken), termController, asset); + repoTokenListData.validateAndInsertRepoToken( + ITermRepoToken(offer.repoToken), termController, discountRateAdapter, asset + ); } prev = current; @@ -174,7 +178,7 @@ library TermAuctionList { function getPresentValue( TermAuctionListData storage listData, RepoTokenListData storage repoTokenListData, - ITermController termController, + ITermDiscountRateAdapter discountRateAdapter, uint256 purchaseTokenPrecision, address repoTokenToMatch ) internal view returns (uint256 totalValue) { @@ -205,7 +209,7 @@ library TermAuctionList { repoTokenAmountInBaseAssetPrecision, purchaseTokenPrecision, RepoTokenList.getRepoTokenMaturity(offer.repoToken), - RepoTokenList.getAuctionRate(termController, ITermRepoToken(offer.repoToken)) + discountRateAdapter.getDiscountRate(offer.repoToken) ); // since multiple offers can be tied to the same repo token, we need to mark diff --git a/src/TermDiscountRateAdapter.sol b/src/TermDiscountRateAdapter.sol new file mode 100644 index 00000000..9b7c4251 --- /dev/null +++ b/src/TermDiscountRateAdapter.sol @@ -0,0 +1,23 @@ +// SPDX-License-Identifier: AGPL-3.0 +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"; + +contract TermDiscountRateAdapter is ITermDiscountRateAdapter { + ITermController public immutable TERM_CONTROLLER; + + constructor(address termController_) { + TERM_CONTROLLER = ITermController(termController_); + } + + function getDiscountRate(address repoToken) external view returns (uint256) { + (AuctionMetadata[] memory auctionMetadata, ) = TERM_CONTROLLER.getTermAuctionResults(ITermRepoToken(repoToken).termRepoId()); + + uint256 len = auctionMetadata.length; + require(len > 0); + + return auctionMetadata[len - 1].auctionClearingRate; + } +} diff --git a/src/TermVaultEventEmitter.sol b/src/TermVaultEventEmitter.sol index 1777e61f..3509f7aa 100644 --- a/src/TermVaultEventEmitter.sol +++ b/src/TermVaultEventEmitter.sol @@ -67,6 +67,13 @@ contract TermVaultEventEmitter is Initializable, UUPSUpgradeable, AccessControlU emit Unpaused(); } + function emitDiscountRateAdapterUpdated( + address oldAdapter, + address newAdapter + ) external onlyRole(VAULT_CONTRACT) { + emit DiscountRateAdapterUpdated(oldAdapter, newAdapter); + } + // ======================================================================== // = Admin =============================================================== // ======================================================================== diff --git a/src/interfaces/term/ITermDiscountRateAdapter.sol b/src/interfaces/term/ITermDiscountRateAdapter.sol new file mode 100644 index 00000000..b3f5d67a --- /dev/null +++ b/src/interfaces/term/ITermDiscountRateAdapter.sol @@ -0,0 +1,6 @@ +// SPDX-License-Identifier: AGPL-3.0 +pragma solidity 0.8.18; + +interface ITermDiscountRateAdapter { + function getDiscountRate(address repoToken) external view returns (uint256); +} diff --git a/src/interfaces/term/ITermVaultEvents.sol b/src/interfaces/term/ITermVaultEvents.sol index b1b96b59..4544371a 100644 --- a/src/interfaces/term/ITermVaultEvents.sol +++ b/src/interfaces/term/ITermVaultEvents.sol @@ -18,6 +18,11 @@ interface ITermVaultEvents { event Unpaused(); + event DiscountRateAdapterUpdated( + address indexed oldAdapter, + address indexed newAdapter + ); + function emitTermControllerUpdated(address oldController, address newController) external; function emitTimeToMaturityThresholdUpdated(uint256 oldThreshold, uint256 newThreshold) external; @@ -33,4 +38,9 @@ interface ITermVaultEvents { function emitPaused() external; function emitUnpaused() external; + + function emitDiscountRateAdapterUpdated( + address oldAdapter, + address newAdapter + ) external; } diff --git a/src/test/TestUSDCSellRepoToken.t.sol b/src/test/TestUSDCSellRepoToken.t.sol index 063df090..ec56c001 100644 --- a/src/test/TestUSDCSellRepoToken.t.sol +++ b/src/test/TestUSDCSellRepoToken.t.sol @@ -363,7 +363,7 @@ contract TestUSDCSellRepoToken is Setup { repoTokenMatured.mint(testUser, 1000e18); // test: token has no auction clearing rate - vm.expectRevert(abi.encodeWithSelector(RepoTokenList.InvalidRepoToken.selector, address(repoToken1Week))); + vm.expectRevert(); vm.prank(testUser); termStrategy.sellRepoToken(address(repoToken1Week), 1e18); diff --git a/src/test/utils/Setup.sol b/src/test/utils/Setup.sol index f9cbd9e5..f6a6077b 100644 --- a/src/test/utils/Setup.sol +++ b/src/test/utils/Setup.sol @@ -5,6 +5,7 @@ import "forge-std/console.sol"; import {ExtendedTest} from "./ExtendedTest.sol"; import {Strategy, ERC20} from "../../Strategy.sol"; +import {TermDiscountRateAdapter} from "../../TermDiscountRateAdapter.sol"; import {IStrategyInterface} from "../../interfaces/IStrategyInterface.sol"; // Inherit the events so they can be checked if desired. @@ -68,6 +69,7 @@ contract Setup is ExtendedTest, IEvents { // Term finance mocks MockTermController internal termController; + TermDiscountRateAdapter internal discountRateAdapter; TermVaultEventEmitter internal termVaultEventEmitterImpl; TermVaultEventEmitter internal termVaultEventEmitter; ERC4626Mock internal mockYearnVault; @@ -93,6 +95,7 @@ contract Setup is ExtendedTest, IEvents { vm.etch(0xBB51273D6c746910C7C06fe718f30c936170feD0, address(tokenizedStrategy).code); termController = new MockTermController(); + discountRateAdapter = new TermDiscountRateAdapter(address(termController)); termVaultEventEmitterImpl = new TermVaultEventEmitter(); termVaultEventEmitter = TermVaultEventEmitter(address(new ERC1967Proxy(address(termVaultEventEmitterImpl), ""))); mockYearnVault = new ERC4626Mock(address(asset)); @@ -116,7 +119,13 @@ contract Setup is ExtendedTest, IEvents { function setUpStrategy() public returns (address) { // we save the strategy as a IStrategyInterface to give it the needed interface IStrategyInterface _strategy = IStrategyInterface( - address(new Strategy(address(asset), "Tokenized Strategy", address(mockYearnVault), address(termVaultEventEmitter))) + address(new Strategy( + address(asset), + "Tokenized Strategy", + address(mockYearnVault), + address(discountRateAdapter), + address(termVaultEventEmitter) + )) ); vm.prank(adminWallet);