diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5a0eeff2..8de8903e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -26,6 +26,9 @@ jobs: with: registry-token: ${{ secrets.GH_REGISTRY_ACCESS_TOKEN }} + - name: Install dependencies + run: yarn install + - name: Run solhint run: yarn lint:check @@ -51,6 +54,9 @@ jobs: with: registry-token: ${{ secrets.GH_REGISTRY_ACCESS_TOKEN }} + - name: Install dependencies + run: yarn install --frozen-lockfile + - name: Compile foundry run: yarn compile --sizes @@ -224,12 +230,13 @@ jobs: run: forge build --build-info --skip */test/** */scripts/** --force - name: "Run Slither analysis" - uses: "crytic/slither-action@v0.3.0" + uses: "crytic/slither-action@v0.3.2" id: "slither" with: ignore-compile: true fail-on: "none" sarif: "results.sarif" + slither-version: "0.10.1" - name: "Upload SARIF file to GitHub code scanning" uses: "github/codeql-action/upload-sarif@v2" @@ -239,4 +246,4 @@ jobs: - name: "Add Slither summary" run: | echo "## Slither result" >> $GITHUB_STEP_SUMMARY - echo "✅ Uploaded to GitHub code scanning" >> $GITHUB_STEP_SUMMARY + echo "✅ Uploaded to GitHub code scanning" >> $GITHUB_STEP_SUMMARY \ No newline at end of file diff --git a/.gitignore b/.gitignore index 969ae7ff..950dc79d 100644 --- a/.gitignore +++ b/.gitignore @@ -18,6 +18,7 @@ export typechain slither-audit.txt slither +cache # Test output coverage diff --git a/contracts/helpers/Rebalancer.sol b/contracts/helpers/Rebalancer.sol index 42e6a660..2545231a 100644 --- a/contracts/helpers/Rebalancer.sol +++ b/contracts/helpers/Rebalancer.sol @@ -70,9 +70,7 @@ contract Rebalancer is IRebalancer, AccessControl { IERC20(tokenIn).safeTransferFrom(msg.sender, address(this), amountIn); // First, dealing with the allowance of the rebalancer to the Transmuter: this allowance is made infinite // by default - uint256 allowance = IERC20(tokenIn).allowance(address(this), address(TRANSMUTER)); - if (allowance < amountIn) - IERC20(tokenIn).safeIncreaseAllowance(address(TRANSMUTER), type(uint256).max - allowance); + _adjustAllowance(tokenIn, address(TRANSMUTER), amountIn); // Mint agToken from `tokenIn` uint256 amountAgToken = TRANSMUTER.swapExactInput( amountIn, @@ -199,4 +197,13 @@ contract Rebalancer is IRebalancer, AccessControl { revert InvalidParam(); IERC20(token).safeTransfer(to, amount); } + + /*////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + HELPER + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////*/ + + function _adjustAllowance(address token, address sender, uint256 amountIn) internal { + uint256 allowance = IERC20(token).allowance(address(this), sender); + if (allowance < amountIn) IERC20(token).safeIncreaseAllowance(sender, type(uint256).max - allowance); + } } diff --git a/contracts/helpers/RebalancerFlashloan.sol b/contracts/helpers/RebalancerFlashloan.sol new file mode 100644 index 00000000..ab9ea417 --- /dev/null +++ b/contracts/helpers/RebalancerFlashloan.sol @@ -0,0 +1,100 @@ +// SPDX-License-Identifier: GPL-3.0 + +pragma solidity ^0.8.19; + +import "./Rebalancer.sol"; +import { IERC4626 } from "interfaces/external/IERC4626.sol"; +import { IERC3156FlashBorrower } from "oz/interfaces/IERC3156FlashBorrower.sol"; +import { IERC3156FlashLender } from "oz/interfaces/IERC3156FlashLender.sol"; + +/// @title RebalancerFlashloan +/// @author Angle Labs, Inc. +/// @dev Rebalancer contract for a Transmuter with as collaterals a liquid stablecoin and an ERC4626 token +/// using this liquid stablecoin as an asset +contract RebalancerFlashloan is Rebalancer, IERC3156FlashBorrower { + using SafeERC20 for IERC20; + using SafeCast for uint256; + bytes32 public constant CALLBACK_SUCCESS = keccak256("ERC3156FlashBorrower.onFlashLoan"); + + /// @notice Angle stablecoin flashloan contract + IERC3156FlashLender public immutable FLASHLOAN; + + constructor( + IAccessControlManager _accessControlManager, + ITransmuter _transmuter, + IERC3156FlashLender _flashloan + ) Rebalancer(_accessControlManager, _transmuter) { + if (address(_flashloan) == address(0)) revert ZeroAddress(); + FLASHLOAN = _flashloan; + IERC20(AGTOKEN).safeApprove(address(_flashloan), type(uint256).max); + } + + /// @notice Burns `amountStablecoins` for one collateral asset and mints stablecoins from the proceeds of the + /// first burn + /// @dev If `increase` is 1, then the system tries to increase its exposure to the yield bearing asset which means + /// burning stablecoin for the liquid asset, depositing into the ERC4626 vault, then minting the stablecoin + /// @dev This function reverts if the second stablecoin mint gives less than `minAmountOut` of stablecoins + function adjustYieldExposure( + uint256 amountStablecoins, + uint8 increase, + address collateral, + address vault, + uint256 minAmountOut + ) external { + if (!TRANSMUTER.isTrustedSeller(msg.sender)) revert NotTrusted(); + FLASHLOAN.flashLoan( + IERC3156FlashBorrower(address(this)), + address(AGTOKEN), + amountStablecoins, + abi.encode(increase, collateral, vault, minAmountOut) + ); + } + + /// @inheritdoc IERC3156FlashBorrower + function onFlashLoan( + address initiator, + address, + uint256 amount, + uint256 fee, + bytes calldata data + ) external returns (bytes32) { + if (msg.sender != address(FLASHLOAN) || initiator != address(this) || fee != 0) revert NotTrusted(); + (uint256 typeAction, address collateral, address vault, uint256 minAmountOut) = abi.decode( + data, + (uint256, address, address, uint256) + ); + address tokenOut; + address tokenIn; + if (typeAction == 1) { + // Increase yield exposure action: we bring in the ERC4626 token + tokenOut = collateral; + tokenIn = vault; + } else { + // Decrease yield exposure action: we bring in the liquid asset + tokenIn = collateral; + tokenOut = vault; + } + uint256 amountOut = TRANSMUTER.swapExactInput(amount, 0, AGTOKEN, tokenOut, address(this), block.timestamp); + if (typeAction == 1) { + // Granting allowance with the collateral for the vault asset + _adjustAllowance(collateral, vault, amountOut); + amountOut = IERC4626(vault).deposit(amountOut, address(this)); + } else amountOut = IERC4626(vault).redeem(amountOut, address(this), address(this)); + _adjustAllowance(tokenIn, address(TRANSMUTER), amountOut); + uint256 amountStableOut = TRANSMUTER.swapExactInput( + amountOut, + minAmountOut, + tokenIn, + AGTOKEN, + address(this), + block.timestamp + ); + if (amount > amountStableOut) { + uint256 subsidy = amount - amountStableOut; + orders[tokenIn][tokenOut].subsidyBudget -= subsidy.toUint112(); + budget -= subsidy; + emit SubsidyPaid(tokenIn, tokenOut, subsidy); + } + return CALLBACK_SUCCESS; + } +} diff --git a/scripts/DeployRebalancerFlashloan.s.sol b/scripts/DeployRebalancerFlashloan.s.sol new file mode 100644 index 00000000..101718f7 --- /dev/null +++ b/scripts/DeployRebalancerFlashloan.s.sol @@ -0,0 +1,39 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity ^0.8.19; + +import "./utils/Utils.s.sol"; +import { console } from "forge-std/console.sol"; +import { RebalancerFlashloan } from "contracts/helpers/RebalancerFlashloan.sol"; +import { IAccessControlManager } from "contracts/utils/AccessControl.sol"; +import { ITransmuter } from "contracts/interfaces/ITransmuter.sol"; +import { IERC3156FlashLender } from "oz/interfaces/IERC3156FlashLender.sol"; +import "./Constants.s.sol"; +import "oz/interfaces/IERC20.sol"; +import "oz-upgradeable/token/ERC20/extensions/IERC20MetadataUpgradeable.sol"; + +contract DeployRebalancerFlashloan is Utils { + function run() external { + uint256 deployerPrivateKey = vm.envUint("DEPLOYER_PRIVATE_KEY"); + vm.startBroadcast(deployerPrivateKey); + + address deployer = vm.addr(deployerPrivateKey); + console.log("Deployer address: ", deployer); + console.log(address(IAccessControlManager(_chainToContract(CHAIN_SOURCE, ContractType.CoreBorrow)))); + console.log(address(ITransmuter(_chainToContract(CHAIN_SOURCE, ContractType.TransmuterAgUSD)))); + RebalancerFlashloan rebalancer = new RebalancerFlashloan( + IAccessControlManager(_chainToContract(CHAIN_SOURCE, ContractType.CoreBorrow)), + ITransmuter(_chainToContract(CHAIN_SOURCE, ContractType.TransmuterAgUSD)), + IERC3156FlashLender(0x4A2FF9bC686A0A23DA13B6194C69939189506F7F) + ); + /* + RebalancerFlashloan rebalancer = new RebalancerFlashloan( + IAccessControlManager(0x5bc6BEf80DA563EBf6Df6D6913513fa9A7ec89BE), + ITransmuter(0x222222fD79264BBE280b4986F6FEfBC3524d0137), + IERC3156FlashLender(0x4A2FF9bC686A0A23DA13B6194C69939189506F7F) + ); + */ + console.log("Rebalancer deployed at: ", address(rebalancer)); + + vm.stopBroadcast(); + } +} diff --git a/scripts/UpdateTransmuterFacets.s.sol b/scripts/UpdateTransmuterFacets.s.sol index 79374955..571ad743 100644 --- a/scripts/UpdateTransmuterFacets.s.sol +++ b/scripts/UpdateTransmuterFacets.s.sol @@ -19,7 +19,6 @@ import "contracts/transmuter/libraries/LibHelpers.sol"; import { ITransmuter } from "interfaces/ITransmuter.sol"; import "utils/src/Constants.sol"; import { IERC20 } from "oz/interfaces/IERC20.sol"; -import { OldTransmuter } from "test/scripts/UpdateTransmuterFacets.t.sol"; contract UpdateTransmuterFacets is Helpers { string[] replaceFacetNames; diff --git a/test/scripts/UpdateTransmuterFacets.t.sol b/scripts/test/UpdateTransmuterFacets.s.sol similarity index 99% rename from test/scripts/UpdateTransmuterFacets.t.sol rename to scripts/test/UpdateTransmuterFacets.s.sol index 2a86748d..8ae6e5a2 100644 --- a/test/scripts/UpdateTransmuterFacets.t.sol +++ b/scripts/test/UpdateTransmuterFacets.s.sol @@ -5,9 +5,9 @@ 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 "../Constants.s.sol"; -import { Helpers } from "../../scripts/Helpers.s.sol"; +import { Helpers } from "../Helpers.s.sol"; import "contracts/utils/Errors.sol" as Errors; import "contracts/transmuter/Storage.sol" as Storage; import { Getters } from "contracts/transmuter/facets/Getters.sol"; @@ -53,7 +53,7 @@ contract UpdateTransmuterFacetsTest is Helpers, Test { CHAIN_SOURCE = CHAIN_ETHEREUM; ethereumFork = vm.createFork(vm.envString("ETH_NODE_URI_MAINNET"), 19425035); - vm.selectFork(forkIdentifier[CHAIN_SOURCE]); + vm.selectFork(ethereumFork); governor = _chainToContract(CHAIN_SOURCE, ContractType.Timelock); transmuter = ITransmuter(_chainToContract(CHAIN_SOURCE, ContractType.TransmuterAgEUR)); diff --git a/test/scripts/UpdateTransmuterFacetsUSDATest.t.sol b/scripts/test/UpdateTransmuterFacetsUSDATest.s.sol similarity index 99% rename from test/scripts/UpdateTransmuterFacetsUSDATest.t.sol rename to scripts/test/UpdateTransmuterFacetsUSDATest.s.sol index 43f34038..7497eb7f 100644 --- a/test/scripts/UpdateTransmuterFacetsUSDATest.t.sol +++ b/scripts/test/UpdateTransmuterFacetsUSDATest.s.sol @@ -5,9 +5,9 @@ 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 "../Constants.s.sol"; -import { Helpers } from "../../scripts/Helpers.s.sol"; +import { Helpers } from "../Helpers.s.sol"; import "contracts/utils/Errors.sol" as Errors; import "contracts/transmuter/Storage.sol" as Storage; import { Getters } from "contracts/transmuter/facets/Getters.sol"; @@ -21,7 +21,7 @@ import { CollateralSetupProd } from "contracts/transmuter/configs/ProductionType import { ITransmuter } from "interfaces/ITransmuter.sol"; import "utils/src/Constants.sol"; import { IERC20 } from "oz/interfaces/IERC20.sol"; -import { IMorphoOracle, MockMorphoOracle } from "../mock/MockMorphoOracle.sol"; +import { IMorphoOracle, MockMorphoOracle } from "../../test/mock/MockMorphoOracle.sol"; interface OldTransmuter { function getOracle( @@ -54,7 +54,7 @@ contract UpdateTransmuterFacetsUSDATest is Helpers, Test { CHAIN_SOURCE = CHAIN_ETHEREUM; ethereumFork = vm.createFork(vm.envString("ETH_NODE_URI_MAINNET"), 19483530); - vm.selectFork(forkIdentifier[CHAIN_SOURCE]); + vm.selectFork(ethereumFork); governor = DEPLOYER; transmuter = ITransmuter(0x222222fD79264BBE280b4986F6FEfBC3524d0137); diff --git a/test/fuzz/OracleTest.t.sol b/test/fuzz/OracleTest.t.sol index 5adba20d..351cedf5 100644 --- a/test/fuzz/OracleTest.t.sol +++ b/test/fuzz/OracleTest.t.sol @@ -983,21 +983,19 @@ contract OracleTest is Fixture, FunctionUtils { } function _getReadType(uint8 newReadType) internal pure returns (Storage.OracleReadType readType) { - readType = newReadType == 0 - ? Storage.OracleReadType.CHAINLINK_FEEDS - : newReadType == 1 - ? Storage.OracleReadType.EXTERNAL - : newReadType == 2 - ? Storage.OracleReadType.NO_ORACLE - : newReadType == 3 - ? Storage.OracleReadType.STABLE - : newReadType == 4 - ? Storage.OracleReadType.WSTETH - : newReadType == 5 - ? Storage.OracleReadType.CBETH - : newReadType == 6 - ? Storage.OracleReadType.RETH - : Storage.OracleReadType.SFRXETH; + readType = newReadType == 0 ? Storage.OracleReadType.CHAINLINK_FEEDS : newReadType == 1 + ? Storage.OracleReadType.EXTERNAL + : newReadType == 2 + ? Storage.OracleReadType.NO_ORACLE + : newReadType == 3 + ? Storage.OracleReadType.STABLE + : newReadType == 4 + ? Storage.OracleReadType.WSTETH + : newReadType == 5 + ? Storage.OracleReadType.CBETH + : newReadType == 6 + ? Storage.OracleReadType.RETH + : Storage.OracleReadType.SFRXETH; } function _updateOracles( diff --git a/test/fuzz/RebalancerFlashloanTest.t.sol b/test/fuzz/RebalancerFlashloanTest.t.sol new file mode 100644 index 00000000..efd613d8 --- /dev/null +++ b/test/fuzz/RebalancerFlashloanTest.t.sol @@ -0,0 +1,89 @@ +// 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"; + +contract RebalancerFlashloanTest is Fixture, FunctionUtils { + using SafeERC20 for IERC20; + + RebalancerFlashloan public rebalancer; + Savings internal _saving; + string internal _name; + string internal _symbol; + address public collat; + + function setUp() public override { + super.setUp(); + + MockTokenPermit token = new MockTokenPermit("EURC", "EURC", 6); + collat = address(token); + + address _savingImplementation = address(new Savings()); + bytes memory data; + _saving = Savings(_deployUpgradeable(address(proxyAdmin), address(_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(); + + rebalancer = new RebalancerFlashloan(accessControlManager, transmuter, IERC3156FlashLender(governor)); + } + + function test_RebalancerInitialization() public { + assertEq(address(rebalancer.accessControlManager()), address(accessControlManager)); + assertEq(address(rebalancer.AGTOKEN()), address(agToken)); + assertEq(address(rebalancer.TRANSMUTER()), address(transmuter)); + assertEq(address(rebalancer.FLASHLOAN()), governor); + assertEq(IERC20Metadata(address(agToken)).allowance(address(rebalancer), address(governor)), type(uint256).max); + assertEq(IERC20Metadata(address(collat)).allowance(address(rebalancer), address(_saving)), 0); + } + + function test_Constructor_RevertWhen_ZeroAddress() public { + vm.expectRevert(Errors.ZeroAddress.selector); + new RebalancerFlashloan(accessControlManager, transmuter, IERC3156FlashLender(address(0))); + } + + function test_adjustYieldExposure_RevertWhen_NotTrusted() public { + vm.expectRevert(Errors.NotTrusted.selector); + rebalancer.adjustYieldExposure(1, 1, address(0), address(0), 0); + } + + function test_onFlashLoan_RevertWhen_NotTrusted() public { + vm.expectRevert(Errors.NotTrusted.selector); + rebalancer.onFlashLoan(address(rebalancer), address(0), 1, 0, abi.encode(1)); + + vm.expectRevert(Errors.NotTrusted.selector); + rebalancer.onFlashLoan(address(rebalancer), address(0), 1, 1, abi.encode(1)); + + vm.expectRevert(Errors.NotTrusted.selector); + vm.startPrank(governor); + rebalancer.onFlashLoan(address(0), address(0), 1, 0, abi.encode(1)); + vm.stopPrank(); + + vm.expectRevert(Errors.NotTrusted.selector); + vm.startPrank(governor); + rebalancer.onFlashLoan(address(rebalancer), address(0), 1, 1, abi.encode(1)); + vm.stopPrank(); + + vm.expectRevert(); + vm.startPrank(governor); + rebalancer.onFlashLoan(address(rebalancer), address(0), 1, 0, abi.encode(1, 2)); + vm.stopPrank(); + } +} diff --git a/test/scripts/RebalancerUSDATest.t.sol b/test/scripts/RebalancerUSDATest.t.sol new file mode 100644 index 00000000..aadc7d55 --- /dev/null +++ b/test/scripts/RebalancerUSDATest.t.sol @@ -0,0 +1,322 @@ +// 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 { 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 RebalancerUSDATest is Test { + using stdJson for string; + + ITransmuter transmuter; + IERC20 USDA; + IAgToken treasuryUSDA; + IFlashAngle FLASHLOAN; + address governor; + RebalancerFlashloan public rebalancer; + uint256 ethereumFork; + + function setUp() public { + ethereumFork = vm.createSelectFork(vm.envString("ETH_NODE_URI_MAINNET"), 19610333); + + transmuter = ITransmuter(0x222222fD79264BBE280b4986F6FEfBC3524d0137); + USDA = IERC20(0x0000206329b97DB379d5E1Bf586BbDB969C63274); + FLASHLOAN = IFlashAngle(0x4A2FF9bC686A0A23DA13B6194C69939189506F7F); + treasuryUSDA = IAgToken(0xf8588520E760BB0b3bDD62Ecb25186A28b0830ee); + governor = 0xdC4e6DFe07EFCa50a197DF15D9200883eF4Eb1c8; + + vm.startPrank(governor); + transmuter.toggleWhitelist(Storage.WhitelistType.BACKED, NEW_DEPLOYER); + transmuter.toggleTrusted(governor, Storage.TrustedType.Seller); + IAgToken(treasuryUSDA).addMinter(address(FLASHLOAN)); + vm.stopPrank(); + + // Setup rebalancer + rebalancer = new RebalancerFlashloan( + // Mock access control manager for USDA + IAccessControlManager(0x3fc5a1bd4d0A435c55374208A6A81535A1923039), + transmuter, + IERC3156FlashLender(address(FLASHLOAN)) + ); + + // Setup flashloan + // Core contract + vm.startPrank(0x5bc6BEf80DA563EBf6Df6D6913513fa9A7ec89BE); + FLASHLOAN.addStablecoinSupport(address(treasuryUSDA)); + vm.stopPrank(); + // Governor address + vm.startPrank(governor); + FLASHLOAN.setFlashLoanParameters(address(USDA), 0, type(uint256).max); + vm.stopPrank(); + + // Initialize Transmuter reserves + deal(BIB01, NEW_DEPLOYER, 100000 * BASE_18); + vm.startPrank(NEW_DEPLOYER); + IERC20(BIB01).approve(address(transmuter), type(uint256).max); + transmuter.swapExactOutput( + 1200 * 10 ** 21, + type(uint256).max, + BIB01, + address(USDA), + NEW_DEPLOYER, + block.timestamp + ); + vm.stopPrank(); + } + + /*////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + GETTERS + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////*/ + + function testUnit_RebalancerSetup() external { + assertEq(address(transmuter.agToken()), 0x0000206329b97DB379d5E1Bf586BbDB969C63274); + // Revert when no order has been setup + vm.startPrank(NEW_DEPLOYER); + vm.expectRevert(); + rebalancer.adjustYieldExposure(BASE_18, 1, USDC, STEAK_USDC, 0); + + vm.expectRevert(); + rebalancer.adjustYieldExposure(BASE_18, 0, USDC, STEAK_USDC, 0); + vm.stopPrank(); + } + + function testFuzz_adjustYieldExposure_SuccessIncrease(uint256 amount) external { + amount = bound(amount, BASE_18, BASE_18 * 100); + deal(address(USDA), address(rebalancer), BASE_18 * 1000); + (uint256 fromUSDC, uint256 total) = transmuter.getIssuedByCollateral(address(USDC)); + (uint256 fromSTEAK, ) = transmuter.getIssuedByCollateral(address(STEAK_USDC)); + vm.startPrank(governor); + rebalancer.setOrder(address(STEAK_USDC), address(USDC), BASE_18 * 500, 0); + rebalancer.setOrder(address(USDC), address(STEAK_USDC), BASE_18 * 500, 0); + + uint256 budget = rebalancer.budget(); + (uint112 orderBudget0, , , ) = rebalancer.orders(address(USDC), address(STEAK_USDC)); + (uint112 orderBudget1, , , ) = rebalancer.orders(address(STEAK_USDC), address(USDC)); + rebalancer.adjustYieldExposure(amount, 1, USDC, STEAK_USDC, 0); + + vm.stopPrank(); + + (uint256 fromUSDCPost, uint256 totalPost) = transmuter.getIssuedByCollateral(address(USDC)); + (uint256 fromSTEAKPost, ) = transmuter.getIssuedByCollateral(address(STEAK_USDC)); + assertLe(totalPost, total); + assertEq(fromUSDCPost, fromUSDC - amount); + assertLe(fromSTEAKPost, fromSTEAK + amount); + assertGe(fromSTEAKPost, fromSTEAK); + assertLe(rebalancer.budget(), budget); + (uint112 newOrder0, , , ) = rebalancer.orders(address(USDC), address(STEAK_USDC)); + (uint112 newOrder1, , , ) = rebalancer.orders(address(STEAK_USDC), address(USDC)); + assertEq(newOrder0, orderBudget0); + assertLe(newOrder1, orderBudget1); + } + + function testFuzz_adjustYieldExposure_RevertMinAmountOut(uint256 amount) external { + amount = bound(amount, BASE_18, BASE_18 * 100); + deal(address(USDA), address(rebalancer), BASE_18 * 1000); + vm.startPrank(governor); + rebalancer.setOrder(address(STEAK_USDC), address(USDC), BASE_18 * 500, 0); + rebalancer.setOrder(address(USDC), address(STEAK_USDC), BASE_18 * 500, 0); + vm.expectRevert(); + rebalancer.adjustYieldExposure(amount, 1, USDC, STEAK_USDC, type(uint256).max); + vm.stopPrank(); + } + + function testFuzz_adjustYieldExposure_SuccessDecrease(uint256 amount) external { + amount = bound(amount, BASE_18, BASE_18 * 100); + deal(address(USDA), address(rebalancer), BASE_18 * 1000); + (uint256 fromUSDC, uint256 total) = transmuter.getIssuedByCollateral(address(USDC)); + (uint256 fromSTEAK, ) = transmuter.getIssuedByCollateral(address(STEAK_USDC)); + vm.startPrank(governor); + rebalancer.setOrder(address(STEAK_USDC), address(USDC), BASE_18 * 500, 0); + rebalancer.setOrder(address(USDC), address(STEAK_USDC), BASE_18 * 500, 0); + uint256 budget = rebalancer.budget(); + (uint112 orderBudget0, , , ) = rebalancer.orders(address(USDC), address(STEAK_USDC)); + (uint112 orderBudget1, , , ) = rebalancer.orders(address(STEAK_USDC), address(USDC)); + rebalancer.adjustYieldExposure(amount, 0, USDC, STEAK_USDC, 0); + vm.stopPrank(); + (uint256 fromUSDCPost, uint256 totalPost) = transmuter.getIssuedByCollateral(address(USDC)); + (uint256 fromSTEAKPost, ) = transmuter.getIssuedByCollateral(address(STEAK_USDC)); + assertLe(totalPost, total); + assertLe(fromUSDCPost, fromUSDC + amount); + assertGe(fromUSDCPost, fromUSDC); + assertEq(fromSTEAKPost, fromSTEAK - amount); + assertLe(rebalancer.budget(), budget); + (uint112 newOrder0, , , ) = rebalancer.orders(address(USDC), address(STEAK_USDC)); + (uint112 newOrder1, , , ) = rebalancer.orders(address(STEAK_USDC), address(USDC)); + assertLe(newOrder0, orderBudget0); + assertEq(newOrder1, orderBudget1); + } + + function testFuzz_adjustYieldExposure_SuccessNoBudgetIncrease(uint256 amount) external { + amount = bound(amount, BASE_18, BASE_18 * 100); + deal(address(USDA), address(rebalancer), BASE_18 * 1000); + (uint256 fromUSDC, uint256 total) = transmuter.getIssuedByCollateral(address(USDC)); + (uint256 fromSTEAK, ) = transmuter.getIssuedByCollateral(address(STEAK_USDC)); + vm.startPrank(governor); + uint64[] memory xMintFee = new uint64[](1); + xMintFee[0] = uint64(0); + int64[] memory yMintFee = new int64[](1); + yMintFee[0] = int64(0); + uint64[] memory xBurnFee = new uint64[](1); + xBurnFee[0] = uint64(BASE_9); + int64[] memory yBurnFee = new int64[](1); + yBurnFee[0] = int64(uint64(0)); + transmuter.setFees(STEAK_USDC, xMintFee, yMintFee, true); + transmuter.setFees(STEAK_USDC, xBurnFee, yBurnFee, false); + assertEq(rebalancer.budget(), 0); + + transmuter.setFees(STEAK_USDC, xMintFee, yMintFee, true); + transmuter.updateOracle(STEAK_USDC); + rebalancer.adjustYieldExposure(amount, 1, USDC, STEAK_USDC, 0); + vm.stopPrank(); + (uint256 fromUSDCPost, uint256 totalPost) = transmuter.getIssuedByCollateral(address(USDC)); + (uint256 fromSTEAKPost, ) = transmuter.getIssuedByCollateral(address(STEAK_USDC)); + assertGe(totalPost, total); + assertEq(fromUSDCPost, fromUSDC - amount); + assertGe(fromSTEAKPost, fromSTEAK + amount); + } + + function testFuzz_adjustYieldExposure_SuccessDecreaseSplit(uint256 amount, uint256 split) external { + amount = bound(amount, BASE_18, BASE_18 * 100); + split = bound(split, BASE_9 / 4, (BASE_9 * 3) / 4); + deal(address(USDA), address(rebalancer), BASE_18 * 1000); + (uint256 fromUSDC, uint256 total) = transmuter.getIssuedByCollateral(address(USDC)); + (uint256 fromSTEAK, ) = transmuter.getIssuedByCollateral(address(STEAK_USDC)); + vm.startPrank(governor); + rebalancer.setOrder(address(STEAK_USDC), address(USDC), BASE_18 * 500, 0); + rebalancer.setOrder(address(USDC), address(STEAK_USDC), BASE_18 * 500, 0); + uint256 budget = rebalancer.budget(); + (uint112 orderBudget0, , , ) = rebalancer.orders(address(USDC), address(STEAK_USDC)); + (uint112 orderBudget1, , , ) = rebalancer.orders(address(STEAK_USDC), address(USDC)); + + rebalancer.adjustYieldExposure((amount * split) / BASE_9, 0, USDC, STEAK_USDC, 0); + + (uint256 fromUSDCPost, uint256 totalPost) = transmuter.getIssuedByCollateral(address(USDC)); + (uint256 fromSTEAKPost, ) = transmuter.getIssuedByCollateral(address(STEAK_USDC)); + assertLe(totalPost, total); + assertLe(fromUSDCPost, fromUSDC + (amount * split) / BASE_9); + assertGe(fromUSDCPost, fromUSDC); + assertEq(fromSTEAKPost, fromSTEAK - (amount * split) / BASE_9); + assertLe(rebalancer.budget(), budget); + + rebalancer.adjustYieldExposure(amount - (amount * split) / BASE_9, 0, USDC, STEAK_USDC, 0); + + (fromUSDCPost, totalPost) = transmuter.getIssuedByCollateral(address(USDC)); + (fromSTEAKPost, ) = transmuter.getIssuedByCollateral(address(STEAK_USDC)); + assertLe(totalPost, total); + assertLe(fromUSDCPost, fromUSDC + amount); + assertGe(fromUSDCPost, fromUSDC); + assertEq(fromSTEAKPost, fromSTEAK - amount); + assertLe(rebalancer.budget(), budget); + (uint112 newOrder0, , , ) = rebalancer.orders(address(USDC), address(STEAK_USDC)); + (uint112 newOrder1, , , ) = rebalancer.orders(address(STEAK_USDC), address(USDC)); + assertLe(newOrder0, orderBudget0); + assertEq(newOrder1, orderBudget1); + + vm.stopPrank(); + } + + function testFuzz_adjustYieldExposure_SuccessIncreaseSplit(uint256 amount, uint256 split) external { + amount = bound(amount, BASE_18, BASE_18 * 100); + split = bound(split, BASE_9 / 4, (BASE_9 * 3) / 4); + deal(address(USDA), address(rebalancer), BASE_18 * 1000); + (uint256 fromUSDC, uint256 total) = transmuter.getIssuedByCollateral(address(USDC)); + (uint256 fromSTEAK, ) = transmuter.getIssuedByCollateral(address(STEAK_USDC)); + vm.startPrank(governor); + rebalancer.setOrder(address(STEAK_USDC), address(USDC), BASE_18 * 500, 0); + rebalancer.setOrder(address(USDC), address(STEAK_USDC), BASE_18 * 500, 0); + uint256 budget = rebalancer.budget(); + (uint112 orderBudget0, , , ) = rebalancer.orders(address(USDC), address(STEAK_USDC)); + (uint112 orderBudget1, , , ) = rebalancer.orders(address(STEAK_USDC), address(USDC)); + + rebalancer.adjustYieldExposure((amount * split) / BASE_9, 1, USDC, STEAK_USDC, 0); + + (uint256 fromUSDCPost, uint256 totalPost) = transmuter.getIssuedByCollateral(address(USDC)); + (uint256 fromSTEAKPost, ) = transmuter.getIssuedByCollateral(address(STEAK_USDC)); + assertLe(totalPost, total); + assertEq(fromUSDCPost, fromUSDC - (amount * split) / BASE_9); + assertLe(fromSTEAKPost, fromSTEAK + (amount * split) / BASE_9); + assertGe(fromSTEAKPost, fromSTEAK); + assertLe(rebalancer.budget(), budget); + + rebalancer.adjustYieldExposure(amount - (amount * split) / BASE_9, 1, USDC, STEAK_USDC, 0); + + (fromUSDCPost, totalPost) = transmuter.getIssuedByCollateral(address(USDC)); + (fromSTEAKPost, ) = transmuter.getIssuedByCollateral(address(STEAK_USDC)); + assertLe(totalPost, total); + assertEq(fromUSDCPost, fromUSDC - amount); + assertLe(fromSTEAKPost, fromSTEAK + amount); + assertGe(fromSTEAKPost, fromSTEAK); + assertLe(rebalancer.budget(), budget); + (uint112 newOrder0, , , ) = rebalancer.orders(address(USDC), address(STEAK_USDC)); + (uint112 newOrder1, , , ) = rebalancer.orders(address(STEAK_USDC), address(USDC)); + assertEq(newOrder0, orderBudget0); + assertLe(newOrder1, orderBudget1); + + vm.stopPrank(); + } + + function testFuzz_adjustYieldExposure_SuccessAltern(uint256 amount) external { + amount = bound(amount, BASE_18, BASE_18 * 100); + deal(address(USDA), address(rebalancer), BASE_18 * 1000); + (uint256 fromUSDC, uint256 total) = transmuter.getIssuedByCollateral(address(USDC)); + (uint256 fromSTEAK, ) = transmuter.getIssuedByCollateral(address(STEAK_USDC)); + vm.startPrank(governor); + rebalancer.setOrder(address(STEAK_USDC), address(USDC), BASE_18 * 500, 0); + rebalancer.setOrder(address(USDC), address(STEAK_USDC), BASE_18 * 500, 0); + uint256 budget = rebalancer.budget(); + (uint112 orderBudget0, , , ) = rebalancer.orders(address(USDC), address(STEAK_USDC)); + (uint112 orderBudget1, , , ) = rebalancer.orders(address(STEAK_USDC), address(USDC)); + + rebalancer.adjustYieldExposure(amount, 1, USDC, STEAK_USDC, 0); + + (uint256 fromUSDCPost, uint256 totalPost) = transmuter.getIssuedByCollateral(address(USDC)); + (uint256 fromSTEAKPost, ) = transmuter.getIssuedByCollateral(address(STEAK_USDC)); + assertLe(totalPost, total); + assertEq(fromUSDCPost, fromUSDC - amount); + assertLe(fromSTEAKPost, fromSTEAK + amount); + assertGe(fromSTEAKPost, fromSTEAK); + assertLe(rebalancer.budget(), budget); + (uint112 newOrder0, , , ) = rebalancer.orders(address(USDC), address(STEAK_USDC)); + (uint112 newOrder1, , , ) = rebalancer.orders(address(STEAK_USDC), address(USDC)); + assertEq(newOrder0, orderBudget0); + assertLe(newOrder1, orderBudget1); + + rebalancer.adjustYieldExposure(amount, 0, USDC, STEAK_USDC, 0); + + (orderBudget0, , , ) = rebalancer.orders(address(USDC), address(STEAK_USDC)); + (orderBudget1, , , ) = rebalancer.orders(address(STEAK_USDC), address(USDC)); + assertLe(orderBudget0, newOrder0); + assertEq(orderBudget1, newOrder1); + + (uint256 fromUSDCPost2, uint256 totalPost2) = transmuter.getIssuedByCollateral(address(USDC)); + (uint256 fromSTEAKPost2, ) = transmuter.getIssuedByCollateral(address(STEAK_USDC)); + + assertLe(totalPost2, totalPost); + assertLe(fromUSDCPost2, fromUSDC); + assertLe(fromSTEAKPost2, fromSTEAK); + assertLe(fromSTEAKPost2, fromSTEAKPost); + assertGe(fromUSDCPost2, fromUSDCPost); + assertLe(rebalancer.budget(), budget); + + vm.stopPrank(); + } +}