From 7276b20b8499e22e11461502d9adb257fa2e0a2d Mon Sep 17 00:00:00 2001 From: Andrew Zhou Date: Fri, 20 Sep 2024 15:07:58 -0700 Subject: [PATCH 1/5] integrative unit test --- src/test/TestUSDCIntegration.t.sol | 144 +++++++++++++++++++++++++++++ 1 file changed, 144 insertions(+) create mode 100644 src/test/TestUSDCIntegration.t.sol diff --git a/src/test/TestUSDCIntegration.t.sol b/src/test/TestUSDCIntegration.t.sol new file mode 100644 index 00000000..93d34ce7 --- /dev/null +++ b/src/test/TestUSDCIntegration.t.sol @@ -0,0 +1,144 @@ +pragma solidity ^0.8.18; + +import "forge-std/console2.sol"; +import {ERC20Mock} from "@openzeppelin/contracts/mocks/ERC20Mock.sol"; +import {MockTermRepoToken} from "./mocks/MockTermRepoToken.sol"; +import {MockTermAuction} from "./mocks/MockTermAuction.sol"; +import {MockUSDC} from "./mocks/MockUSDC.sol"; +import {Setup, ERC20, IStrategyInterface} from "./utils/Setup.sol"; +import {Strategy} from "../Strategy.sol"; + +contract TestUSDCIntegration is Setup { + uint256 internal constant TEST_REPO_TOKEN_RATE = 0.05e18; + uint256 public constant THREESIXTY_DAYCOUNT_SECONDS = 360 days; + uint256 public constant RATE_PRECISION = 1e18; + + MockUSDC internal mockUSDC; + ERC20Mock internal mockCollateral; + MockTermRepoToken internal repoToken1Week; + MockTermRepoToken internal repoToken1Month; + MockTermAuction internal repoToken1WeekAuction; + Strategy internal termStrategy; + StrategySnapshot internal initialState; + + function setUp() public override { + mockUSDC = new MockUSDC(); + mockCollateral = new ERC20Mock(); + + _setUp(ERC20(address(mockUSDC))); + + repoToken1Week = new MockTermRepoToken( + bytes32("test repo token 1"), address(mockUSDC), address(mockCollateral), 1e18, 1 weeks + ); + repoToken1Month = new MockTermRepoToken( + bytes32("test repo token 2"), address(mockUSDC), address(mockCollateral), 1e18, 4 weeks + ); + termController.setOracleRate(MockTermRepoToken(repoToken1Week).termRepoId(), TEST_REPO_TOKEN_RATE); + termController.setOracleRate(MockTermRepoToken(repoToken1Month).termRepoId(), TEST_REPO_TOKEN_RATE); + + + termStrategy = Strategy(address(strategy)); + + repoToken1WeekAuction = new MockTermAuction(repoToken1Week); + + vm.startPrank(management); + 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 + mockUSDC.mint(address(strategy), 100e6); + + initialState.totalAssetValue = termStrategy.totalAssetValue(); + initialState.totalLiquidBalance = termStrategy.totalLiquidBalance(); + } + + function _submitOffer(bytes32 idHash, uint256 offerAmount) private returns (bytes32) { + // test: only management can submit offers + vm.expectRevert("!management"); + bytes32[] memory offerIds = termStrategy.submitAuctionOffer( + repoToken1WeekAuction, address(repoToken1Week), idHash, bytes32("test price"), offerAmount + ); + + vm.prank(management); + offerIds = termStrategy.submitAuctionOffer( + repoToken1WeekAuction, address(repoToken1Week), idHash, bytes32("test price"), offerAmount + ); + + assertEq(offerIds.length, 1); + + return offerIds[0]; + } + + function testSellRepoTokenSubmitOfferAndCloseAuction() public { + address testUser = vm.addr(0x11111); + mockUSDC.mint(testUser, 1e18); + repoToken1Month.mint(testUser, 1000e18); + + vm.startPrank(testUser); + mockUSDC.approve(address(mockYearnVault), type(uint256).max); + mockYearnVault.deposit(1e18, testUser); + repoToken1Month.approve(address(strategy), type(uint256).max); + termStrategy.sellRepoToken(address(repoToken1Month), 1e6); + mockYearnVault.withdraw(1e18, testUser, testUser); + vm.stopPrank(); + + address[] memory holdings = termStrategy.repoTokenHoldings(); + + assertEq(holdings.length, 1); + + + bytes32 offerId1 = _submitOffer(bytes32("offer id hash 1"), 1e6); + bytes32[] memory offerIds = new bytes32[](1); + offerIds[0] = offerId1; + uint256[] memory fillAmounts = new uint256[](1); + fillAmounts[0] = 1e6; + uint256[] memory repoTokenAmounts = new uint256[](1); + repoTokenAmounts[0] = _getRepoTokenAmountGivenPurchaseTokenAmount( + 1e6, repoToken1Week, TEST_REPO_TOKEN_RATE + ); + + + repoToken1WeekAuction.auctionSuccess(offerIds, fillAmounts, repoTokenAmounts); + + holdings = termStrategy.repoTokenHoldings(); + + // test: 1 holding because auctionClosed not yet called + assertEq(holdings.length, 1); + + termStrategy.auctionClosed(); + holdings = termStrategy.repoTokenHoldings(); + assertEq(holdings.length, 2); + bytes32[] memory offers = termStrategy.pendingOffers(); + + assertEq(offers.length, 0); + + // assertEq(termStrategy.totalLiquidBalance(), initialState.totalLiquidBalance - 1e6); + // test: totalAssetValue = total liquid balance + pending offer amount + // assertEq(termStrategy.totalAssetValue(), termStrategy.totalLiquidBalance() + 1e6); + } + + function _getRepoTokenAmountGivenPurchaseTokenAmount( + uint256 purchaseTokenAmount, + MockTermRepoToken termRepoToken, + uint256 discountRate + ) private view returns (uint256) { + (uint256 redemptionTimestamp, address purchaseToken, ,) = termRepoToken.config(); + + uint256 purchaseTokenPrecision = 10**ERC20(purchaseToken).decimals(); + uint256 repoTokenPrecision = 10**ERC20(address(termRepoToken)).decimals(); + + uint256 timeLeftToMaturityDayFraction = + ((redemptionTimestamp - block.timestamp) * purchaseTokenPrecision) / THREESIXTY_DAYCOUNT_SECONDS; + + // purchaseTokenAmount * (1 + r * days / 360) = repoTokenAmountInBaseAssetPrecision + uint256 repoTokenAmountInBaseAssetPrecision = + purchaseTokenAmount * (purchaseTokenPrecision + (discountRate * timeLeftToMaturityDayFraction / RATE_PRECISION)) / purchaseTokenPrecision; + + return repoTokenAmountInBaseAssetPrecision * repoTokenPrecision / purchaseTokenPrecision; + } + +} From 8b315fed7a920e34578745a4ce9b33031f3a3fa9 Mon Sep 17 00:00:00 2001 From: Andrew Zhou Date: Mon, 23 Sep 2024 15:29:22 -0700 Subject: [PATCH 2/5] Update src/test/TestUSDCIntegration.t.sol Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- src/test/TestUSDCIntegration.t.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/TestUSDCIntegration.t.sol b/src/test/TestUSDCIntegration.t.sol index 93d34ce7..faa00de9 100644 --- a/src/test/TestUSDCIntegration.t.sol +++ b/src/test/TestUSDCIntegration.t.sol @@ -46,7 +46,7 @@ contract TestUSDCIntegration is Setup { termStrategy.setTimeToMaturityThreshold(3 weeks); termStrategy.setRepoTokenConcentrationLimit(1e18); termStrategy.setRequiredReserveRatio(0); - termStrategy.setdiscountRateMarkup(0); + termStrategy.setDiscountRateMarkup(0); vm.stopPrank(); // start with some initial funds From 311c47d26d1e013dae91aee38d5fdd1cfd07260e Mon Sep 17 00:00:00 2001 From: Andrew Zhou Date: Mon, 23 Sep 2024 15:41:25 -0700 Subject: [PATCH 3/5] check if token holdings are sorted by maturity --- src/test/TestUSDCIntegration.t.sol | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/test/TestUSDCIntegration.t.sol b/src/test/TestUSDCIntegration.t.sol index faa00de9..bc6e4572 100644 --- a/src/test/TestUSDCIntegration.t.sol +++ b/src/test/TestUSDCIntegration.t.sol @@ -112,13 +112,16 @@ contract TestUSDCIntegration is Setup { termStrategy.auctionClosed(); holdings = termStrategy.repoTokenHoldings(); assertEq(holdings.length, 2); + (uint256 holdings0Maturity, , ,) = MockTermRepoToken(holdings[0]).config(); + (uint256 holdings1Maturity, , ,) = MockTermRepoToken(holdings[1]).config(); + assert(holdings0Maturity <= holdings1Maturity); bytes32[] memory offers = termStrategy.pendingOffers(); assertEq(offers.length, 0); - // assertEq(termStrategy.totalLiquidBalance(), initialState.totalLiquidBalance - 1e6); + assertEq(termStrategy.totalLiquidBalance(), initialState.totalLiquidBalance - 1e6); // test: totalAssetValue = total liquid balance + pending offer amount - // assertEq(termStrategy.totalAssetValue(), termStrategy.totalLiquidBalance() + 1e6); + assertEq(termStrategy.totalAssetValue(), termStrategy.totalLiquidBalance() + 1e6); } function _getRepoTokenAmountGivenPurchaseTokenAmount( From 4faa1d24443f1e1e8db73cf1725320bb86d1776f Mon Sep 17 00:00:00 2001 From: Andrew Zhou Date: Mon, 23 Sep 2024 15:51:21 -0700 Subject: [PATCH 4/5] change back to incorrectly styled functio nname --- src/test/TestUSDCIntegration.t.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/TestUSDCIntegration.t.sol b/src/test/TestUSDCIntegration.t.sol index bc6e4572..32d6bb61 100644 --- a/src/test/TestUSDCIntegration.t.sol +++ b/src/test/TestUSDCIntegration.t.sol @@ -46,7 +46,7 @@ contract TestUSDCIntegration is Setup { termStrategy.setTimeToMaturityThreshold(3 weeks); termStrategy.setRepoTokenConcentrationLimit(1e18); termStrategy.setRequiredReserveRatio(0); - termStrategy.setDiscountRateMarkup(0); + termStrategy.setdiscountRateMarkup(0); vm.stopPrank(); // start with some initial funds From 355a8d90db77eebb192192593e924ea97fc614c7 Mon Sep 17 00:00:00 2001 From: Andrew Zhou Date: Mon, 23 Sep 2024 16:24:35 -0700 Subject: [PATCH 5/5] add test to make sure offer list is sorted by auction address --- src/test/TestUSDCIntegration.t.sol | 38 +++++++++++++++++++++++++----- 1 file changed, 32 insertions(+), 6 deletions(-) diff --git a/src/test/TestUSDCIntegration.t.sol b/src/test/TestUSDCIntegration.t.sol index 32d6bb61..2e44aabf 100644 --- a/src/test/TestUSDCIntegration.t.sol +++ b/src/test/TestUSDCIntegration.t.sol @@ -18,6 +18,7 @@ contract TestUSDCIntegration is Setup { MockTermRepoToken internal repoToken1Week; MockTermRepoToken internal repoToken1Month; MockTermAuction internal repoToken1WeekAuction; + MockTermAuction internal repoToken1MonthAuction; Strategy internal termStrategy; StrategySnapshot internal initialState; @@ -40,13 +41,14 @@ contract TestUSDCIntegration is Setup { termStrategy = Strategy(address(strategy)); repoToken1WeekAuction = new MockTermAuction(repoToken1Week); + repoToken1MonthAuction = new MockTermAuction(repoToken1Month); vm.startPrank(management); termStrategy.setCollateralTokenParams(address(mockCollateral), 0.5e18); termStrategy.setTimeToMaturityThreshold(3 weeks); termStrategy.setRepoTokenConcentrationLimit(1e18); termStrategy.setRequiredReserveRatio(0); - termStrategy.setdiscountRateMarkup(0); + termStrategy.setDiscountRateMarkup(0); vm.stopPrank(); // start with some initial funds @@ -56,16 +58,16 @@ contract TestUSDCIntegration is Setup { initialState.totalLiquidBalance = termStrategy.totalLiquidBalance(); } - function _submitOffer(bytes32 idHash, uint256 offerAmount) private returns (bytes32) { + function _submitOffer(bytes32 idHash, uint256 offerAmount, MockTermAuction auction, MockTermRepoToken repoToken) private returns (bytes32) { // test: only management can submit offers vm.expectRevert("!management"); bytes32[] memory offerIds = termStrategy.submitAuctionOffer( - repoToken1WeekAuction, address(repoToken1Week), idHash, bytes32("test price"), offerAmount + auction, address(repoToken), idHash, bytes32("test price"), offerAmount ); vm.prank(management); offerIds = termStrategy.submitAuctionOffer( - repoToken1WeekAuction, address(repoToken1Week), idHash, bytes32("test price"), offerAmount + auction, address(repoToken), idHash, bytes32("test price"), offerAmount ); assertEq(offerIds.length, 1); @@ -91,7 +93,7 @@ contract TestUSDCIntegration is Setup { assertEq(holdings.length, 1); - bytes32 offerId1 = _submitOffer(bytes32("offer id hash 1"), 1e6); + bytes32 offerId1 = _submitOffer(bytes32("offer id hash 1"), 1e6, repoToken1WeekAuction, repoToken1Week); bytes32[] memory offerIds = new bytes32[](1); offerIds[0] = offerId1; uint256[] memory fillAmounts = new uint256[](1); @@ -114,7 +116,7 @@ contract TestUSDCIntegration is Setup { assertEq(holdings.length, 2); (uint256 holdings0Maturity, , ,) = MockTermRepoToken(holdings[0]).config(); (uint256 holdings1Maturity, , ,) = MockTermRepoToken(holdings[1]).config(); - assert(holdings0Maturity <= holdings1Maturity); + assertTrue(holdings0Maturity <= holdings1Maturity); bytes32[] memory offers = termStrategy.pendingOffers(); assertEq(offers.length, 0); @@ -124,6 +126,30 @@ contract TestUSDCIntegration is Setup { assertEq(termStrategy.totalAssetValue(), termStrategy.totalLiquidBalance() + 1e6); } + function testSubmittingOffersToMultipleAuctions() public { + address testUser = vm.addr(0x11111); + mockUSDC.mint(testUser, 1e18); + repoToken1Month.mint(testUser, 1000e18); + + vm.startPrank(testUser); + mockUSDC.approve(address(mockYearnVault), type(uint256).max); + mockYearnVault.deposit(1e18, testUser); + repoToken1Month.approve(address(strategy), type(uint256).max); + termStrategy.sellRepoToken(address(repoToken1Month), 1e6); + mockYearnVault.withdraw(1e18, testUser, testUser); + vm.stopPrank(); + + bytes32 offerId1 = _submitOffer(bytes32("offer id hash 1"), 1e6, repoToken1WeekAuction, repoToken1Week); + + bytes32 offerId2 = _submitOffer(bytes32("offer id hash 2"), 1e6, repoToken1MonthAuction, repoToken1Month); + + bytes32[] memory offers = termStrategy.pendingOffers(); + + assertEq(offers.length, 2); + + assertTrue(offers[0] == offerId1 ? address(repoToken1WeekAuction) <= address(repoToken1MonthAuction) : address(repoToken1MonthAuction) <= address(repoToken1WeekAuction)); + } + function _getRepoTokenAmountGivenPurchaseTokenAmount( uint256 purchaseTokenAmount, MockTermRepoToken termRepoToken,