From e1e9ea243f6ca8a35ca9045b6bfa1e8f0835d59a Mon Sep 17 00:00:00 2001 From: Pablo Veyrat Date: Fri, 24 May 2024 14:39:49 +0200 Subject: [PATCH] finishing harvesting tests --- contracts/helpers/Harvester.sol | 65 +++---- test/fuzz/Harvester.t.sol | 244 +++++++++++++++++++++++++++ test/scripts/HarvesterUSDATest.t.sol | 226 +++++++++++++++++++++++++ 3 files changed, 506 insertions(+), 29 deletions(-) create mode 100644 test/fuzz/Harvester.t.sol create mode 100644 test/scripts/HarvesterUSDATest.t.sol diff --git a/contracts/helpers/Harvester.sol b/contracts/helpers/Harvester.sol index a47a5b42..df5b37ed 100644 --- a/contracts/helpers/Harvester.sol +++ b/contracts/helpers/Harvester.sol @@ -15,6 +15,19 @@ import "../utils/Errors.sol"; import { RebalancerFlashloan } from "./RebalancerFlashloan.sol"; +struct CollatParams { + // Vault associated to the collateral + address vault; + // Target exposure to the collateral asset used in the vault + uint64 targetExposure; + // Maximum exposure within the Transmuter to the vault asset + uint64 maxExposureYieldAsset; + // Minimum exposure within the Transmuter to the vault asset + uint64 minExposureYieldAsset; + // Whether limit exposures should be overriden or read onchain through the Transmuter + uint64 overrideExposures; +} + /// @title Harvester /// @author Angle Labs, Inc. /// @dev Contract for anyone to permissionlessly adjust the reserves of Angle Transmuter through the RebalancerFlashloan contract @@ -22,19 +35,6 @@ contract Harvester is AccessControl { using SafeERC20 for IERC20; using SafeCast for uint256; - struct CollateralSetup { - // Vault associated to the collateral - address vault; - // Target exposure to the collateral asset used in the vault - uint64 targetExposure; - // Maximum exposure within the Transmuter to the vault asset - uint64 maxExposureYieldAsset; - // Minimum exposure within the Transmuter to the vault asset - uint64 minExposureYieldAsset; - // Whether limit exposures should be overriden or read onchain through the Transmuter - uint64 overrideExposures; - } - /// @notice Reference to the `transmuter` implementation this contract aims at rebalancing ITransmuter public immutable TRANSMUTER; /// @notice Permissioned rebalancer contract @@ -42,25 +42,27 @@ contract Harvester is AccessControl { /// @notice Max slippage when dealing with the Transmuter uint96 public maxSlippage; /// @notice Data associated to a collateral - mapping(address => CollateralSetup) public collateralData; + mapping(address => CollatParams) public collateralData; /*////////////////////////////////////////////////////////////////////////////////////////////////////////////////// INITIALIZATION //////////////////////////////////////////////////////////////////////////////////////////////////////////////////*/ constructor( - RebalancerFlashloan _rebalancer, + address _rebalancer, address vault, uint64 targetExposure, uint64 overrideExposures, uint64 maxExposureYieldAsset, - uint64 minExposureYieldAsset + uint64 minExposureYieldAsset, + uint96 _maxSlippage ) { - ITransmuter transmuter = _rebalancer.TRANSMUTER(); + ITransmuter transmuter = RebalancerFlashloan(_rebalancer).TRANSMUTER(); TRANSMUTER = transmuter; - rebalancer = _rebalancer; + rebalancer = RebalancerFlashloan(_rebalancer); accessControlManager = IAccessControlManager(transmuter.accessControlManager()); _setCollateralData(vault, targetExposure, minExposureYieldAsset, maxExposureYieldAsset, overrideExposures); + _setMaxSlippage(_maxSlippage); } /*////////////////////////////////////////////////////////////////////////////////////////////////////////////////// @@ -74,15 +76,15 @@ contract Harvester is AccessControl { /// to the target exposure function harvest(address collateral) external { (uint256 stablecoinsFromCollateral, uint256 stablecoinsIssued) = TRANSMUTER.getIssuedByCollateral(collateral); - CollateralSetup memory collatInfo = collateralData[collateral]; + CollatParams memory collatInfo = collateralData[collateral]; (uint256 stablecoinsFromVault, ) = TRANSMUTER.getIssuedByCollateral(collatInfo.vault); uint8 increase; uint256 amount; - uint256 targetExposureLiquid = collatInfo.targetExposure; - if (stablecoinsFromCollateral * 1e9 > targetExposureLiquid * stablecoinsIssued) { + uint256 targetExposureScaled = collatInfo.targetExposure * stablecoinsIssued; + if (stablecoinsFromCollateral * 1e9 > targetExposureScaled) { // Need to increase exposure to yield bearing asset increase = 1; - amount = stablecoinsFromCollateral - (targetExposureLiquid * stablecoinsIssued) / 1e9; + amount = stablecoinsFromCollateral - targetExposureScaled / 1e9; uint256 maxValueScaled = collatInfo.maxExposureYieldAsset * stablecoinsIssued; // These checks assume that there are no transaction fees on the stablecoin->collateral conversion and so // it's still possible that exposure goes above the max exposure in some rare cases @@ -92,14 +94,15 @@ contract Harvester is AccessControl { } else { // In this case, exposure after the operation might remain slightly below the targetExposure as less // collateral may be obtained by burning stablecoins for the yield asset and unwrapping it - amount = (targetExposureLiquid * stablecoinsIssued) / 1e9 - stablecoinsFromCollateral; + amount = targetExposureScaled / 1e9 - stablecoinsFromCollateral; uint256 minValueScaled = collatInfo.minExposureYieldAsset * stablecoinsIssued; if (stablecoinsFromVault * 1e9 < minValueScaled) amount = 0; else if (stablecoinsFromVault * 1e9 < minValueScaled + amount * 1e9) amount = stablecoinsFromVault - minValueScaled / 1e9; } if (amount > 0) { - TRANSMUTER.updateOracle(collatInfo.vault); + try TRANSMUTER.updateOracle(collatInfo.vault) {} catch {} + rebalancer.adjustYieldExposure( amount, increase, @@ -130,15 +133,19 @@ contract Harvester is AccessControl { } function setMaxSlippage(uint96 _maxSlippage) external onlyGuardian { - if (maxSlippage > 1e9) revert InvalidParam(); - maxSlippage = _maxSlippage; + _setMaxSlippage(_maxSlippage); } function updateLimitExposuresYieldAsset(address collateral) external { - CollateralSetup storage collatInfo = collateralData[collateral]; + CollatParams storage collatInfo = collateralData[collateral]; if (collatInfo.overrideExposures == 0) _updateLimitExposuresYieldAsset(collatInfo); } + function _setMaxSlippage(uint96 _maxSlippage) internal { + if (_maxSlippage > 1e9) revert InvalidParam(); + maxSlippage = _maxSlippage; + } + function _setCollateralData( address vault, uint64 targetExposure, @@ -147,7 +154,7 @@ contract Harvester is AccessControl { uint64 overrideExposures ) internal { address collateral = address(IERC4626(vault).asset()); - CollateralSetup storage collatInfo = collateralData[collateral]; + CollatParams storage collatInfo = collateralData[collateral]; collatInfo.vault = vault; if (targetExposure >= 1e9) revert InvalidParam(); collatInfo.targetExposure = targetExposure; @@ -161,7 +168,7 @@ contract Harvester is AccessControl { } } - function _updateLimitExposuresYieldAsset(CollateralSetup storage collatInfo) internal { + function _updateLimitExposuresYieldAsset(CollatParams storage collatInfo) internal { uint64[] memory xFeeMint; (xFeeMint, ) = TRANSMUTER.getCollateralMintFees(collatInfo.vault); uint256 length = xFeeMint.length; diff --git a/test/fuzz/Harvester.t.sol b/test/fuzz/Harvester.t.sol new file mode 100644 index 00000000..6234d0ff --- /dev/null +++ b/test/fuzz/Harvester.t.sol @@ -0,0 +1,244 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.19; + +import { SafeERC20 } from "oz/token/ERC20/utils/SafeERC20.sol"; + +import { stdError } from "forge-std/Test.sol"; + +import "contracts/utils/Errors.sol" as Errors; + +import "../Fixture.sol"; +import { IERC20Metadata } from "../mock/MockTokenPermit.sol"; +import "../utils/FunctionUtils.sol"; + +import "contracts/savings/Savings.sol"; +import "../mock/MockTokenPermit.sol"; +import "contracts/helpers/RebalancerFlashloan.sol"; +import "contracts/helpers/Harvester.sol"; + +contract HarvesterTest is Fixture, FunctionUtils { + using SafeERC20 for IERC20; + + RebalancerFlashloan public rebalancer; + Harvester public harvester; + Savings internal _saving; + address internal _savingImplementation; + string internal _name; + string internal _symbol; + address public collat; + uint64 public targetExposure; + uint64 public maxExposureYieldAsset; + uint64 public minExposureYieldAsset; + + function setUp() public override { + super.setUp(); + + MockTokenPermit token = new MockTokenPermit("EURC", "EURC", 6); + collat = address(token); + + _savingImplementation = address(new Savings()); + bytes memory data; + _saving = Savings(_deployUpgradeable(address(proxyAdmin), _savingImplementation, data)); + _name = "savingAgEUR"; + _symbol = "SAGEUR"; + + vm.startPrank(governor); + token.mint(governor, 1e12); + token.approve(address(_saving), 1e12); + _saving.initialize(accessControlManager, IERC20MetadataUpgradeable(address(token)), _name, _symbol, BASE_6); + vm.stopPrank(); + targetExposure = uint64((15 * 1e9) / 100); + maxExposureYieldAsset = uint64((80 * 1e9) / 100); + minExposureYieldAsset = uint64((5 * 1e9) / 100); + rebalancer = new RebalancerFlashloan(accessControlManager, transmuter, IERC3156FlashLender(governor)); + harvester = new Harvester( + address(rebalancer), + address(_saving), + targetExposure, + 1, + maxExposureYieldAsset, + minExposureYieldAsset, + 1e8 + ); + } + + function test_RebalancerInitialization() public { + assertEq(address(harvester.rebalancer()), address(rebalancer)); + assertEq(address(harvester.TRANSMUTER()), address(transmuter)); + assertEq(address(harvester.accessControlManager()), address(accessControlManager)); + (address vault, uint64 target, uint64 maxi, uint64 mini, uint64 overrideExp) = harvester.collateralData(collat); + assertEq(vault, address(_saving)); + assertEq(target, targetExposure); + assertEq(maxi, maxExposureYieldAsset); + assertEq(mini, minExposureYieldAsset); + assertEq(overrideExp, 1); + } + + function test_Constructor_RevertWhen_InvalidParams() public { + vm.expectRevert(); + new Harvester( + address(0), + address(_saving), + targetExposure, + 1, + maxExposureYieldAsset, + minExposureYieldAsset, + 1e8 + ); + + vm.expectRevert(); + new Harvester( + address(rebalancer), + address(0), + targetExposure, + 1, + maxExposureYieldAsset, + minExposureYieldAsset, + 1e8 + ); + + vm.expectRevert(Errors.InvalidParam.selector); + harvester = new Harvester( + address(rebalancer), + address(_saving), + 1e10, + 1, + maxExposureYieldAsset, + minExposureYieldAsset, + 1e8 + ); + + vm.expectRevert(Errors.InvalidParam.selector); + harvester = new Harvester( + address(rebalancer), + address(_saving), + 1e10, + 0, + maxExposureYieldAsset, + minExposureYieldAsset, + 1e8 + ); + + vm.expectRevert(Errors.InvalidParam.selector); + harvester = new Harvester(address(rebalancer), address(_saving), 1e9 / 10, 1, 1e10, minExposureYieldAsset, 1e8); + + vm.expectRevert(Errors.InvalidParam.selector); + harvester = new Harvester(address(rebalancer), address(_saving), 1e9 / 10, 1, 1e8, 2e8, 1e8); + + vm.expectRevert(Errors.InvalidParam.selector); + harvester = new Harvester( + address(rebalancer), + address(_saving), + targetExposure, + 1, + maxExposureYieldAsset, + minExposureYieldAsset, + 1e10 + ); + } + + function test_OnlyGuardian_RevertWhen_NotGuardian() public { + vm.expectRevert(Errors.NotGovernorOrGuardian.selector); + harvester.setRebalancer(alice); + + vm.expectRevert(Errors.NotGovernorOrGuardian.selector); + harvester.setCollateralData(address(_saving), targetExposure, 1, maxExposureYieldAsset, minExposureYieldAsset); + + vm.expectRevert(Errors.NotGovernorOrGuardian.selector); + harvester.setMaxSlippage(1e9); + + harvester.updateLimitExposuresYieldAsset(collat); + } + + function test_SettersHarvester() public { + vm.startPrank(governor); + vm.expectRevert(Errors.InvalidParam.selector); + harvester.setMaxSlippage(1e10); + + harvester.setMaxSlippage(123456); + assertEq(harvester.maxSlippage(), 123456); + + vm.expectRevert(Errors.ZeroAddress.selector); + harvester.setRebalancer(address(0)); + + harvester.setRebalancer(address(harvester)); + assertEq(address(harvester.rebalancer()), address(harvester)); + + harvester.setCollateralData( + address(_saving), + targetExposure + 10, + minExposureYieldAsset - 1, + maxExposureYieldAsset + 1, + 1 + ); + (address vault, uint64 target, uint64 maxi, uint64 mini, uint64 overrideExp) = harvester.collateralData(collat); + assertEq(vault, address(_saving)); + assertEq(target, targetExposure + 10); + assertEq(maxi, maxExposureYieldAsset + 1); + assertEq(mini, minExposureYieldAsset - 1); + assertEq(overrideExp, 1); + + harvester.setCollateralData( + address(_saving), + targetExposure + 10, + minExposureYieldAsset - 1, + maxExposureYieldAsset + 1, + 0 + ); + (vault, target, maxi, mini, overrideExp) = harvester.collateralData(collat); + assertEq(vault, address(_saving)); + assertEq(target, targetExposure + 10); + assertEq(maxi, 1e9); + assertEq(mini, 0); + assertEq(overrideExp, 0); + + vm.stopPrank(); + } + + function test_UpdateLimitExposuresYieldAsset() public { + bytes memory data; + Savings newVault = Savings(_deployUpgradeable(address(proxyAdmin), _savingImplementation, data)); + _name = "savingAgEUR"; + _symbol = "SAGEUR"; + + vm.startPrank(governor); + MockTokenPermit(address(eurA)).mint(governor, 1e12); + eurA.approve(address(newVault), 1e12); + newVault.initialize(accessControlManager, IERC20MetadataUpgradeable(address(eurA)), _name, _symbol, BASE_6); + transmuter.addCollateral(address(newVault)); + vm.stopPrank(); + + uint64[] memory xFeeMint = new uint64[](3); + int64[] memory yFeeMint = new int64[](3); + + xFeeMint[0] = 0; + xFeeMint[1] = uint64((15 * BASE_9) / 100); + xFeeMint[2] = uint64((2 * BASE_9) / 10); + + yFeeMint[0] = int64(1); + yFeeMint[1] = int64(uint64(BASE_9 / 10)); + yFeeMint[2] = int64(uint64((2 * BASE_9) / 10)); + + uint64[] memory xFeeBurn = new uint64[](3); + int64[] memory yFeeBurn = new int64[](3); + + xFeeBurn[0] = uint64(BASE_9); + xFeeBurn[1] = uint64(BASE_9 / 10); + xFeeBurn[2] = 0; + + yFeeBurn[0] = int64(1); + yFeeBurn[1] = int64(1); + yFeeBurn[2] = int64(uint64(BASE_9 / 10)); + + vm.startPrank(governor); + transmuter.setFees(address(newVault), xFeeBurn, yFeeBurn, false); + transmuter.setFees(address(newVault), xFeeMint, yFeeMint, true); + harvester.setCollateralData(address(newVault), targetExposure, minExposureYieldAsset, maxExposureYieldAsset, 0); + harvester.updateLimitExposuresYieldAsset(address(eurA)); + + (, , uint64 maxi, uint64 mini, ) = harvester.collateralData(address(eurA)); + assertEq(maxi, (15 * BASE_9) / 100); + assertEq(mini, BASE_9 / 10); + vm.stopPrank(); + } +} diff --git a/test/scripts/HarvesterUSDATest.t.sol b/test/scripts/HarvesterUSDATest.t.sol new file mode 100644 index 00000000..ac7ddff7 --- /dev/null +++ b/test/scripts/HarvesterUSDATest.t.sol @@ -0,0 +1,226 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity ^0.8.19; + +import { stdJson } from "forge-std/StdJson.sol"; +import { console } from "forge-std/console.sol"; +import { Test } from "forge-std/Test.sol"; + +import "../../scripts/Constants.s.sol"; + +import "contracts/utils/Errors.sol" as Errors; +import "contracts/transmuter/Storage.sol" as Storage; +import "contracts/transmuter/libraries/LibHelpers.sol"; +import { CollateralSetupProd } from "contracts/transmuter/configs/ProductionTypes.sol"; +import { ITransmuter } from "interfaces/ITransmuter.sol"; +import "utils/src/Constants.sol"; +import { IERC20 } from "oz/interfaces/IERC20.sol"; +import { IAgToken } from "interfaces/IAgToken.sol"; +import { Harvester } from "contracts/helpers/Harvester.sol"; + +import { RebalancerFlashloan, IERC4626, IERC3156FlashLender } from "contracts/helpers/RebalancerFlashloan.sol"; + +interface IFlashAngle { + function addStablecoinSupport(address _treasury) external; + + function setFlashLoanParameters(address stablecoin, uint64 _flashLoanFee, uint256 _maxBorrowable) external; +} + +contract HarvesterUSDATest is Test { + using stdJson for string; + + ITransmuter transmuter; + IERC20 USDA; + IAgToken treasuryUSDA; + IFlashAngle FLASHLOAN; + address governor; + RebalancerFlashloan public rebalancer; + uint256 ethereumFork; + Harvester harvester; + uint64 public targetExposure; + uint64 public maxExposureYieldAsset; + uint64 public minExposureYieldAsset; + + function setUp() public { + ethereumFork = vm.createSelectFork(vm.envString("ETH_NODE_URI_MAINNET"), 19939091); + + transmuter = ITransmuter(0x222222fD79264BBE280b4986F6FEfBC3524d0137); + USDA = IERC20(0x0000206329b97DB379d5E1Bf586BbDB969C63274); + FLASHLOAN = IFlashAngle(0x4A2FF9bC686A0A23DA13B6194C69939189506F7F); + treasuryUSDA = IAgToken(0xf8588520E760BB0b3bDD62Ecb25186A28b0830ee); + governor = 0xdC4e6DFe07EFCa50a197DF15D9200883eF4Eb1c8; + rebalancer = RebalancerFlashloan(0x22604C0E5633A9810E01c9cb469B23Eee17AC411); + targetExposure = uint64((15 * 1e9) / 100); + maxExposureYieldAsset = uint64((80 * 1e9) / 100); + minExposureYieldAsset = uint64((5 * 1e9) / 100); + + harvester = new Harvester( + address(rebalancer), + address(STEAK_USDC), + targetExposure, + 1, + maxExposureYieldAsset, + minExposureYieldAsset, + 1e8 + ); + + vm.startPrank(governor); + transmuter.toggleWhitelist(Storage.WhitelistType.BACKED, NEW_DEPLOYER); + transmuter.toggleTrusted(governor, Storage.TrustedType.Seller); + transmuter.toggleTrusted(address(harvester), Storage.TrustedType.Seller); + + vm.stopPrank(); + } + + function testUnit_Harvest_IncreaseUSDCExposure() external { + // At current block: USDC exposure = 7.63%, steakUSDC = 75.26%, bIB01 = 17.11% + (uint256 fromUSDC, uint256 total) = transmuter.getIssuedByCollateral(address(USDC)); + (uint256 fromSTEAK, ) = transmuter.getIssuedByCollateral(address(STEAK_USDC)); + harvester.harvest(USDC); + (uint256 fromUSDC2, uint256 total2) = transmuter.getIssuedByCollateral(address(USDC)); + (uint256 fromSTEAK2, ) = transmuter.getIssuedByCollateral(address(STEAK_USDC)); + assertGt(fromUSDC2, fromUSDC); + assertGt(fromSTEAK, fromSTEAK2); + assertGt(total, total2); + assertApproxEqRel((fromUSDC2 * 1e9) / total2, targetExposure, 100 * BPS); + + harvester.harvest(USDC); + (uint256 fromUSDC3, uint256 total3) = transmuter.getIssuedByCollateral(address(USDC)); + (uint256 fromSTEAK3, ) = transmuter.getIssuedByCollateral(address(STEAK_USDC)); + assertGt(fromUSDC3, fromUSDC2); + assertGt(fromSTEAK2, fromSTEAK3); + assertGt(total2, total3); + assertGt((fromUSDC3 * 1e9) / total3, (fromUSDC2 * 1e9) / total2); + assertGt(targetExposure, (fromUSDC3 * 1e9) / total3); + } + + function testUnit_Harvest_IncreaseUSDCExposureButMinValueYield() external { + // At current block: USDC exposure = 7.63%, steakUSDC = 75.26%, bIB01 = 17.11% -> putting below + // min exposure + vm.startPrank(governor); + harvester.setCollateralData(STEAK_USDC, targetExposure, (80 * 1e9) / 100, (90 * 1e9) / 100, 1); + vm.stopPrank(); + (uint256 fromUSDC, uint256 total) = transmuter.getIssuedByCollateral(address(USDC)); + (uint256 fromSTEAK, ) = transmuter.getIssuedByCollateral(address(STEAK_USDC)); + harvester.harvest(USDC); + (uint256 fromUSDC2, uint256 total2) = transmuter.getIssuedByCollateral(address(USDC)); + (uint256 fromSTEAK2, ) = transmuter.getIssuedByCollateral(address(STEAK_USDC)); + assertEq(fromUSDC2, fromUSDC); + assertEq(fromSTEAK, fromSTEAK2); + assertEq(total, total2); + + harvester.harvest(USDC); + (uint256 fromUSDC3, uint256 total3) = transmuter.getIssuedByCollateral(address(USDC)); + (uint256 fromSTEAK3, ) = transmuter.getIssuedByCollateral(address(STEAK_USDC)); + assertEq(fromUSDC3, fromUSDC2); + assertEq(fromSTEAK2, fromSTEAK3); + assertEq(total2, total3); + } + + function testUnit_Harvest_IncreaseUSDCExposureButMinValueThresholdReached() external { + // At current block: USDC exposure = 7.63%, steakUSDC = 75.26%, bIB01 = 17.11% -> putting in between + // min exposure and target exposure + vm.startPrank(governor); + harvester.setCollateralData(STEAK_USDC, targetExposure, (73 * 1e9) / 100, (90 * 1e9) / 100, 1); + vm.stopPrank(); + + (uint256 fromUSDC, uint256 total) = transmuter.getIssuedByCollateral(address(USDC)); + (uint256 fromSTEAK, ) = transmuter.getIssuedByCollateral(address(STEAK_USDC)); + harvester.harvest(USDC); + (uint256 fromUSDC2, uint256 total2) = transmuter.getIssuedByCollateral(address(USDC)); + (uint256 fromSTEAK2, ) = transmuter.getIssuedByCollateral(address(STEAK_USDC)); + assertGt(fromUSDC2, fromUSDC); + assertGt(fromSTEAK, fromSTEAK2); + assertGt(total, total2); + assertApproxEqRel((fromSTEAK2 * 1e9) / total2, (73 * 1e9) / 100, 100 * BPS); + + harvester.harvest(USDC); + (uint256 fromUSDC3, uint256 total3) = transmuter.getIssuedByCollateral(address(USDC)); + (uint256 fromSTEAK3, ) = transmuter.getIssuedByCollateral(address(STEAK_USDC)); + assertGt(fromUSDC3, fromUSDC2); + assertGt(fromSTEAK2, fromSTEAK3); + assertGt(total2, total3); + assertApproxEqRel((fromSTEAK3 * 1e9) / total3, (fromSTEAK2 * 1e9) / total2, 10 * BPS); + } + + function testUnit_Harvest_DecreaseUSDCExposureClassical() external { + // At current block: USDC exposure = 7.63%, steakUSDC = 75.26%, bIB01 = 17.11% -> putting below target + vm.startPrank(governor); + harvester.setCollateralData(STEAK_USDC, (5 * 1e9) / 100, (73 * 1e9) / 100, (90 * 1e9) / 100, 1); + vm.stopPrank(); + + (uint256 fromUSDC, uint256 total) = transmuter.getIssuedByCollateral(address(USDC)); + (uint256 fromSTEAK, ) = transmuter.getIssuedByCollateral(address(STEAK_USDC)); + + harvester.harvest(USDC); + + (uint256 fromUSDC2, uint256 total2) = transmuter.getIssuedByCollateral(address(USDC)); + (uint256 fromSTEAK2, ) = transmuter.getIssuedByCollateral(address(STEAK_USDC)); + assertGt(fromUSDC, fromUSDC2); + assertGt(fromSTEAK2, fromSTEAK); + assertGt(total, total2); + assertApproxEqRel((fromUSDC2 * 1e9) / total2, (5 * 1e9) / 100, 100 * BPS); + + harvester.harvest(USDC); + (uint256 fromUSDC3, uint256 total3) = transmuter.getIssuedByCollateral(address(USDC)); + (uint256 fromSTEAK3, ) = transmuter.getIssuedByCollateral(address(STEAK_USDC)); + assertGt(fromUSDC2, fromUSDC3); + assertGt(fromSTEAK3, fromSTEAK2); + assertGt(total2, total3); + assertGe((fromUSDC2 * 1e9) / total2, (fromUSDC3 * 1e9) / total3); + assertGe((fromUSDC3 * 1e9) / total3, (5 * 1e9) / 100); + } + + function testUnit_Harvest_DecreaseUSDCExposureAlreadyMaxThreshold() external { + // At current block: USDC exposure = 7.63%, steakUSDC = 75.26%, bIB01 = 17.11% -> putting below target + vm.startPrank(governor); + harvester.setCollateralData(STEAK_USDC, (5 * 1e9) / 100, (73 * 1e9) / 100, (74 * 1e9) / 100, 1); + vm.stopPrank(); + + (uint256 fromUSDC, uint256 total) = transmuter.getIssuedByCollateral(address(USDC)); + (uint256 fromSTEAK, ) = transmuter.getIssuedByCollateral(address(STEAK_USDC)); + + harvester.harvest(USDC); + + (uint256 fromUSDC2, uint256 total2) = transmuter.getIssuedByCollateral(address(USDC)); + (uint256 fromSTEAK2, ) = transmuter.getIssuedByCollateral(address(STEAK_USDC)); + assertEq(fromUSDC, fromUSDC2); + assertEq(fromSTEAK2, fromSTEAK); + assertEq(total, total2); + + harvester.harvest(USDC); + (uint256 fromUSDC3, uint256 total3) = transmuter.getIssuedByCollateral(address(USDC)); + (uint256 fromSTEAK3, ) = transmuter.getIssuedByCollateral(address(STEAK_USDC)); + assertEq(fromUSDC2, fromUSDC3); + assertEq(fromSTEAK3, fromSTEAK2); + assertEq(total2, total3); + } + + function testUnit_Harvest_DecreaseUSDCExposureTillMaxThreshold() external { + // At current block: USDC exposure = 7.63%, steakUSDC = 75.26%, bIB01 = 17.11% -> putting below target + vm.startPrank(governor); + harvester.setCollateralData(STEAK_USDC, (5 * 1e9) / 100, (73 * 1e9) / 100, (755 * 1e9) / 1000, 1); + vm.stopPrank(); + + (uint256 fromUSDC, uint256 total) = transmuter.getIssuedByCollateral(address(USDC)); + (uint256 fromSTEAK, ) = transmuter.getIssuedByCollateral(address(STEAK_USDC)); + + harvester.harvest(USDC); + + (uint256 fromUSDC2, uint256 total2) = transmuter.getIssuedByCollateral(address(USDC)); + (uint256 fromSTEAK2, ) = transmuter.getIssuedByCollateral(address(STEAK_USDC)); + assertGt(fromUSDC, fromUSDC2); + assertGt(fromSTEAK2, fromSTEAK); + assertGt(total, total2); + assertLe((fromSTEAK2 * 1e9) / total2, (755 * 1e9) / 1000); + assertApproxEqRel((fromSTEAK2 * 1e9) / total2, (755 * 1e9) / 1000, 100 * BPS); + + harvester.harvest(USDC); + + (uint256 fromUSDC3, uint256 total3) = transmuter.getIssuedByCollateral(address(USDC)); + (uint256 fromSTEAK3, ) = transmuter.getIssuedByCollateral(address(STEAK_USDC)); + assertGt(fromUSDC2, fromUSDC3); + assertGt(fromSTEAK3, fromSTEAK2); + assertGt(total2, total3); + assertApproxEqRel((fromSTEAK3 * 1e9) / total3, (fromSTEAK2 * 1e9) / total2, 10 * BPS); + } +}