Skip to content

Commit

Permalink
discount rate adapter
Browse files Browse the repository at this point in the history
  • Loading branch information
0xddong committed Aug 7, 2024
1 parent 606c4a3 commit b2e6d25
Show file tree
Hide file tree
Showing 9 changed files with 83 additions and 23 deletions.
18 changes: 4 additions & 14 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,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)];
Expand All @@ -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);

Expand Down
19 changes: 15 additions & 4 deletions src/Strategy.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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();
}
Expand Down Expand Up @@ -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));
Expand Down Expand Up @@ -233,6 +240,7 @@ contract Strategy is BaseStrategy, Pausable, ReentrancyGuard {
(uint256 auctionRate, uint256 redemptionTimestamp) = repoTokenListData.validateAndInsertRepoToken(
ITermRepoToken(repoToken),
termController,
discountRateAdapter,
address(asset)
);

Expand Down Expand Up @@ -307,7 +315,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);
}
Expand Down Expand Up @@ -498,28 +506,31 @@ 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
);
}

function _totalAssetValue() internal view returns (uint256 totalValue) {
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)
);
}

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 Down
10 changes: 7 additions & 3 deletions src/TermAuctionList.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -81,6 +82,7 @@ library TermAuctionList {
TermAuctionListData storage listData,
RepoTokenListData storage repoTokenListData,
ITermController termController,
ITermDiscountRateAdapter discountRateAdapter,
address asset
) internal {
/*
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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
Expand Down
23 changes: 23 additions & 0 deletions src/TermDiscountRateAdapter.sol
Original file line number Diff line number Diff line change
@@ -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;
}
}
7 changes: 7 additions & 0 deletions src/TermVaultEventEmitter.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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 ===============================================================
// ========================================================================
Expand Down
6 changes: 6 additions & 0 deletions src/interfaces/term/ITermDiscountRateAdapter.sol
Original file line number Diff line number Diff line change
@@ -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);
}
10 changes: 10 additions & 0 deletions src/interfaces/term/ITermVaultEvents.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -33,4 +38,9 @@ interface ITermVaultEvents {
function emitPaused() external;

function emitUnpaused() external;

function emitDiscountRateAdapterUpdated(
address oldAdapter,
address newAdapter
) external;
}
2 changes: 1 addition & 1 deletion src/test/TestUSDCSellRepoToken.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down
11 changes: 10 additions & 1 deletion src/test/utils/Setup.sol
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import "forge-std/console2.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.
Expand Down Expand Up @@ -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;
Expand All @@ -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));
Expand All @@ -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);
Expand Down

0 comments on commit b2e6d25

Please sign in to comment.