From 1c417c47b0cecdfc07fd8175b71c20c3524ac593 Mon Sep 17 00:00:00 2001 From: 0xtekgrinder <72015889+0xtekgrinder@users.noreply.github.com> Date: Tue, 3 Sep 2024 15:38:49 +0200 Subject: [PATCH] Feat/harvester-swap (#116) * feat: Harvester and Balancer with swap logic * fix: forwardUtils now sends error * feat: use Swapper from utils * refactor: remove duplicated code * fix: correct asset and collateral for HarvesterVault * fix: handle slippage with different decimals * feat: scale of harvested amount * tests: all rebalancer and harvester swap tests * fix: correct slippage handling for RebalancerSwap * style: remove lint issues * feat: abstract onFlashLoan * fix: script updateFacets tests --- .vscode/settings.json | 2 +- .../{Harvester.sol => BaseHarvester.sol} | 108 +++-- ...shloan.sol => BaseRebalancerFlashloan.sol} | 63 ++- contracts/helpers/HarvesterSwap.sol | 33 ++ contracts/helpers/HarvesterVault.sol | 50 ++ contracts/helpers/RebalancerFlashloanSwap.sol | 94 ++++ .../helpers/RebalancerFlashloanVault.sol | 41 ++ contracts/interfaces/IRebalancer.sol | 4 + contracts/interfaces/IRebalancerFlashloan.sol | 24 + contracts/utils/Errors.sol | 2 + foundry.toml | 12 +- lib/forge-std | 2 +- lib/utils | 2 +- scripts/AdjustYieldExposure.s.sol | 6 +- scripts/Constants.s.sol | 3 + scripts/DeployHarvesterSwap.s.sol | 36 ++ ...ester.s.sol => DeployHarvesterVault.s.sol} | 16 +- scripts/DeployRebalancerFlashloanSwap.s.sol | 36 ++ ...l => DeployRebalancerFlashloanVault.s.sol} | 8 +- scripts/Helpers.s.sol | 37 +- .../test/UpdateTransmuterFacetsUSDATest.s.sol | 5 +- test/fuzz/Harvester.t.sol | 34 +- ...sol => RebalancerFlashloanVaultTest.t.sol} | 12 +- test/mock/MockRouter.sol | 13 + test/scripts/HarvesterSwapUSDATest.t.sol | 136 ++++++ test/scripts/HarvesterUSDATest.t.sol | 45 +- test/scripts/RebalancerSwapUSDATest.t.sol | 452 ++++++++++++++++++ test/scripts/RebalancerUSDATest.t.sol | 30 +- utils/forwardUtils.js | 4 +- 29 files changed, 1118 insertions(+), 192 deletions(-) rename contracts/helpers/{Harvester.sol => BaseHarvester.sol} (70%) rename contracts/helpers/{RebalancerFlashloan.sol => BaseRebalancerFlashloan.sol} (63%) create mode 100644 contracts/helpers/HarvesterSwap.sol create mode 100644 contracts/helpers/HarvesterVault.sol create mode 100644 contracts/helpers/RebalancerFlashloanSwap.sol create mode 100644 contracts/helpers/RebalancerFlashloanVault.sol create mode 100644 contracts/interfaces/IRebalancerFlashloan.sol create mode 100644 scripts/DeployHarvesterSwap.s.sol rename scripts/{DeployHarvester.s.sol => DeployHarvesterVault.s.sol} (63%) create mode 100644 scripts/DeployRebalancerFlashloanSwap.s.sol rename scripts/{DeployRebalancerFlashloan.s.sol => DeployRebalancerFlashloanVault.s.sol} (84%) rename test/fuzz/{RebalancerFlashloanTest.t.sol => RebalancerFlashloanVaultTest.t.sol} (88%) create mode 100644 test/mock/MockRouter.sol create mode 100644 test/scripts/HarvesterSwapUSDATest.t.sol create mode 100644 test/scripts/RebalancerSwapUSDATest.t.sol diff --git a/.vscode/settings.json b/.vscode/settings.json index f40cc54d..41e7d4fe 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -12,7 +12,7 @@ }, "slither.solcPath": "", "slither.hiddenDetectors": [], - "solidity.compileUsingRemoteVersion": "v0.8.22", + "solidity.compileUsingRemoteVersion": "v0.8.23", "files.insertFinalNewline": true, "solidity.remappings": [ "ds-test/=lib/forge-std/lib/ds-test/src/", diff --git a/contracts/helpers/Harvester.sol b/contracts/helpers/BaseHarvester.sol similarity index 70% rename from contracts/helpers/Harvester.sol rename to contracts/helpers/BaseHarvester.sol index 96788642..4196c82f 100644 --- a/contracts/helpers/Harvester.sol +++ b/contracts/helpers/BaseHarvester.sol @@ -2,9 +2,6 @@ pragma solidity ^0.8.23; -import { IERC20 } from "oz/interfaces/IERC20.sol"; -import { IERC4626 } from "interfaces/external/IERC4626.sol"; -import { SafeERC20 } from "oz/token/ERC20/utils/SafeERC20.sol"; import { SafeCast } from "oz/utils/math/SafeCast.sol"; import { ITransmuter } from "interfaces/ITransmuter.sol"; @@ -13,34 +10,32 @@ import { AccessControl, IAccessControlManager } from "../utils/AccessControl.sol import "../utils/Constants.sol"; import "../utils/Errors.sol"; -import { RebalancerFlashloan } from "./RebalancerFlashloan.sol"; +import { IRebalancerFlashloan } from "../interfaces/IRebalancerFlashloan.sol"; struct CollatParams { - // Vault associated to the collateral - address vault; - // Target exposure to the collateral asset used in the vault + // Yield bearing asset associated to the collateral + address asset; + // Target exposure to the collateral asset used uint64 targetExposure; - // Maximum exposure within the Transmuter to the vault asset + // Maximum exposure within the Transmuter to the asset uint64 maxExposureYieldAsset; - // Minimum exposure within the Transmuter to the vault asset + // Minimum exposure within the Transmuter to the asset uint64 minExposureYieldAsset; // Whether limit exposures should be overriden or read onchain through the Transmuter // This value should be 1 to override exposures or 2 if these shouldn't be overriden uint64 overrideExposures; } -/// @title Harvester +/// @title BaseHarvester /// @author Angle Labs, Inc. -/// @dev Contract for anyone to permissionlessly adjust the reserves of Angle Transmuter through -/// the RebalancerFlashloan contract -contract Harvester is AccessControl { - using SafeERC20 for IERC20; +/// @dev Generic contract for anyone to permissionlessly adjust the reserves of Angle Transmuter through +contract BaseHarvester is AccessControl { using SafeCast for uint256; /// @notice Reference to the `transmuter` implementation this contract aims at rebalancing ITransmuter public immutable TRANSMUTER; /// @notice Permissioned rebalancer contract - RebalancerFlashloan public rebalancer; + IRebalancerFlashloan public rebalancer; /// @notice Max slippage when dealing with the Transmuter uint96 public maxSlippage; /// @notice Data associated to a collateral @@ -52,18 +47,26 @@ contract Harvester is AccessControl { constructor( address _rebalancer, - address vault, + address collateral, + address asset, uint64 targetExposure, uint64 overrideExposures, uint64 maxExposureYieldAsset, uint64 minExposureYieldAsset, uint96 _maxSlippage ) { - ITransmuter transmuter = RebalancerFlashloan(_rebalancer).TRANSMUTER(); + ITransmuter transmuter = IRebalancerFlashloan(_rebalancer).TRANSMUTER(); TRANSMUTER = transmuter; - rebalancer = RebalancerFlashloan(_rebalancer); + rebalancer = IRebalancerFlashloan(_rebalancer); accessControlManager = IAccessControlManager(transmuter.accessControlManager()); - _setCollateralData(vault, targetExposure, minExposureYieldAsset, maxExposureYieldAsset, overrideExposures); + _setCollateralData( + collateral, + asset, + targetExposure, + minExposureYieldAsset, + maxExposureYieldAsset, + overrideExposures + ); _setMaxSlippage(_maxSlippage); } @@ -77,12 +80,11 @@ contract Harvester is AccessControl { /// that can then be used for people looking to burn stablecoins /// @dev Due to potential transaction fees within the Transmuter, this function doesn't exactly bring `collateral` /// to the target exposure - /// @dev The `harvest` possibility shouldn't be implemented for assets with a manipulable price (like ERC4626) - /// contracts on which the `previewRedeem` values can be easily moved by creating a loss or a profit - function harvest(address collateral) external { + function harvest(address collateral, uint256 scale, bytes calldata extraData) public virtual { + if (scale > 1e9) revert InvalidParam(); (uint256 stablecoinsFromCollateral, uint256 stablecoinsIssued) = TRANSMUTER.getIssuedByCollateral(collateral); CollatParams memory collatInfo = collateralData[collateral]; - (uint256 stablecoinsFromVault, ) = TRANSMUTER.getIssuedByCollateral(collatInfo.vault); + (uint256 stablecoinsFromAsset, ) = TRANSMUTER.getIssuedByCollateral(collatInfo.asset); uint8 increase; uint256 amount; uint256 targetExposureScaled = collatInfo.targetExposure * stablecoinsIssued; @@ -93,27 +95,29 @@ contract Harvester is AccessControl { 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 - if (stablecoinsFromVault * 1e9 > maxValueScaled) amount = 0; - else if ((stablecoinsFromVault + amount) * 1e9 > maxValueScaled) - amount = maxValueScaled / 1e9 - stablecoinsFromVault; + if (stablecoinsFromAsset * 1e9 > maxValueScaled) amount = 0; + else if ((stablecoinsFromAsset + amount) * 1e9 > maxValueScaled) + amount = maxValueScaled / 1e9 - stablecoinsFromAsset; } 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 = 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 (stablecoinsFromAsset * 1e9 < minValueScaled) amount = 0; + else if (stablecoinsFromAsset * 1e9 < minValueScaled + amount * 1e9) + amount = stablecoinsFromAsset - minValueScaled / 1e9; } + amount = (amount * scale) / 1e9; if (amount > 0) { - try TRANSMUTER.updateOracle(collatInfo.vault) {} catch {} + try TRANSMUTER.updateOracle(collatInfo.asset) {} catch {} rebalancer.adjustYieldExposure( amount, increase, collateral, - collatInfo.vault, - (amount * (1e9 - maxSlippage)) / 1e9 + collatInfo.asset, + (amount * (1e9 - maxSlippage)) / 1e9, + extraData ); } } @@ -122,47 +126,53 @@ contract Harvester is AccessControl { SETTERS //////////////////////////////////////////////////////////////////////////////////////////////////////////////////*/ - function setRebalancer(address _newRebalancer) external onlyGuardian { + function setRebalancer(address _newRebalancer) public virtual onlyGuardian { if (_newRebalancer == address(0)) revert ZeroAddress(); - rebalancer = RebalancerFlashloan(_newRebalancer); + rebalancer = IRebalancerFlashloan(_newRebalancer); } - /// @dev This function shouldn't be called for a vault (e.g an ERC4626 token) which price can be easily moved - /// by creating a loss or a profit, at the risk of depleting the reserves available in the Rebalancer function setCollateralData( - address vault, + address collateral, + address asset, uint64 targetExposure, uint64 minExposureYieldAsset, uint64 maxExposureYieldAsset, uint64 overrideExposures - ) external onlyGuardian { - _setCollateralData(vault, targetExposure, minExposureYieldAsset, maxExposureYieldAsset, overrideExposures); + ) public virtual onlyGuardian { + _setCollateralData( + collateral, + asset, + targetExposure, + minExposureYieldAsset, + maxExposureYieldAsset, + overrideExposures + ); } - function setMaxSlippage(uint96 _maxSlippage) external onlyGuardian { + function setMaxSlippage(uint96 _maxSlippage) public virtual onlyGuardian { _setMaxSlippage(_maxSlippage); } - function updateLimitExposuresYieldAsset(address collateral) external { + function updateLimitExposuresYieldAsset(address collateral) public virtual { CollatParams storage collatInfo = collateralData[collateral]; if (collatInfo.overrideExposures == 2) _updateLimitExposuresYieldAsset(collatInfo); } - function _setMaxSlippage(uint96 _maxSlippage) internal { + function _setMaxSlippage(uint96 _maxSlippage) internal virtual { if (_maxSlippage > 1e9) revert InvalidParam(); maxSlippage = _maxSlippage; } function _setCollateralData( - address vault, + address collateral, + address asset, uint64 targetExposure, uint64 minExposureYieldAsset, uint64 maxExposureYieldAsset, uint64 overrideExposures - ) internal { - address collateral = address(IERC4626(vault).asset()); + ) internal virtual { CollatParams storage collatInfo = collateralData[collateral]; - collatInfo.vault = vault; + collatInfo.asset = asset; if (targetExposure >= 1e9) revert InvalidParam(); collatInfo.targetExposure = targetExposure; collatInfo.overrideExposures = overrideExposures; @@ -176,15 +186,15 @@ contract Harvester is AccessControl { } } - function _updateLimitExposuresYieldAsset(CollatParams storage collatInfo) internal { + function _updateLimitExposuresYieldAsset(CollatParams storage collatInfo) internal virtual { uint64[] memory xFeeMint; - (xFeeMint, ) = TRANSMUTER.getCollateralMintFees(collatInfo.vault); + (xFeeMint, ) = TRANSMUTER.getCollateralMintFees(collatInfo.asset); uint256 length = xFeeMint.length; if (length <= 1) collatInfo.maxExposureYieldAsset = 1e9; else collatInfo.maxExposureYieldAsset = xFeeMint[length - 2]; uint64[] memory xFeeBurn; - (xFeeBurn, ) = TRANSMUTER.getCollateralBurnFees(collatInfo.vault); + (xFeeBurn, ) = TRANSMUTER.getCollateralBurnFees(collatInfo.asset); length = xFeeBurn.length; if (length <= 1) collatInfo.minExposureYieldAsset = 0; else collatInfo.minExposureYieldAsset = xFeeBurn[length - 2]; diff --git a/contracts/helpers/RebalancerFlashloan.sol b/contracts/helpers/BaseRebalancerFlashloan.sol similarity index 63% rename from contracts/helpers/RebalancerFlashloan.sol rename to contracts/helpers/BaseRebalancerFlashloan.sol index ab9ea417..d9f461fb 100644 --- a/contracts/helpers/RebalancerFlashloan.sol +++ b/contracts/helpers/BaseRebalancerFlashloan.sol @@ -3,15 +3,13 @@ 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 +/// @title BaseRebalancerFlashloan /// @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 { +/// @dev General rebalancer contract with flashloan capabilities +contract BaseRebalancerFlashloan is Rebalancer, IERC3156FlashBorrower { using SafeERC20 for IERC20; using SafeCast for uint256; bytes32 public constant CALLBACK_SUCCESS = keccak256("ERC3156FlashBorrower.onFlashLoan"); @@ -29,24 +27,26 @@ contract RebalancerFlashloan is Rebalancer, IERC3156FlashBorrower { 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 + /// @notice Burns `amountStablecoins` for one collateral asset, swap for asset then mints stablecoins + /// from the proceeds of the swap. /// @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 + /// burning stablecoin for the liquid asset, swapping for the yield bearing asset, then minting the stablecoin /// @dev This function reverts if the second stablecoin mint gives less than `minAmountOut` of stablecoins + /// @dev This function reverts if the swap slippage is higher than `maxSlippage` function adjustYieldExposure( uint256 amountStablecoins, uint8 increase, address collateral, - address vault, - uint256 minAmountOut - ) external { + address asset, + uint256 minAmountOut, + bytes calldata extraData + ) public virtual { if (!TRANSMUTER.isTrustedSeller(msg.sender)) revert NotTrusted(); FLASHLOAN.flashLoan( IERC3156FlashBorrower(address(this)), address(AGTOKEN), amountStablecoins, - abi.encode(increase, collateral, vault, minAmountOut) + abi.encode(increase, collateral, asset, minAmountOut, extraData) ); } @@ -57,29 +57,26 @@ contract RebalancerFlashloan is Rebalancer, IERC3156FlashBorrower { uint256 amount, uint256 fee, bytes calldata data - ) external returns (bytes32) { + ) public virtual 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) - ); + (uint256 typeAction, address collateral, address asset, uint256 minAmountOut, bytes memory callData) = abi + .decode(data, (uint256, address, address, uint256, bytes)); address tokenOut; address tokenIn; if (typeAction == 1) { - // Increase yield exposure action: we bring in the ERC4626 token + // Increase yield exposure action: we bring in the yield bearing asset tokenOut = collateral; - tokenIn = vault; + tokenIn = asset; } else { // Decrease yield exposure action: we bring in the liquid asset tokenIn = collateral; - tokenOut = vault; + tokenOut = asset; } 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)); + + // Swap to tokenIn + amountOut = _swapToTokenIn(typeAction, tokenIn, tokenOut, amountOut, callData); + _adjustAllowance(tokenIn, address(TRANSMUTER), amountOut); uint256 amountStableOut = TRANSMUTER.swapExactInput( amountOut, @@ -97,4 +94,20 @@ contract RebalancerFlashloan is Rebalancer, IERC3156FlashBorrower { } return CALLBACK_SUCCESS; } + + /** + * @dev hook to swap from tokenOut to tokenIn + * @param typeAction 1 for deposit, 2 for redeem + * @param tokenIn address of the token to swap + * @param tokenOut address of the token to receive + * @param amount amount of token to swap + * @param callData extra call data (if needed) + */ + function _swapToTokenIn( + uint256 typeAction, + address tokenIn, + address tokenOut, + uint256 amount, + bytes memory callData + ) internal virtual returns (uint256) {} } diff --git a/contracts/helpers/HarvesterSwap.sol b/contracts/helpers/HarvesterSwap.sol new file mode 100644 index 00000000..3ad3d151 --- /dev/null +++ b/contracts/helpers/HarvesterSwap.sol @@ -0,0 +1,33 @@ +// SPDX-License-Identifier: GPL-3.0 + +pragma solidity ^0.8.23; + +import "./BaseHarvester.sol"; + +/// @title HarvesterSwap +/// @author Angle Labs, Inc. +/// @dev Contract for anyone to permissionlessly adjust the reserves of Angle Transmuter through +/// the RebalancerFlashloanSwap contract +contract HarvesterSwap is BaseHarvester { + constructor( + address _rebalancer, + address collateral, + address asset, + uint64 targetExposure, + uint64 overrideExposures, + uint64 maxExposureYieldAsset, + uint64 minExposureYieldAsset, + uint96 _maxSlippage + ) + BaseHarvester( + _rebalancer, + collateral, + asset, + targetExposure, + overrideExposures, + maxExposureYieldAsset, + minExposureYieldAsset, + _maxSlippage + ) + {} +} diff --git a/contracts/helpers/HarvesterVault.sol b/contracts/helpers/HarvesterVault.sol new file mode 100644 index 00000000..effce3ef --- /dev/null +++ b/contracts/helpers/HarvesterVault.sol @@ -0,0 +1,50 @@ +// SPDX-License-Identifier: GPL-3.0 + +pragma solidity ^0.8.23; + +import "./BaseHarvester.sol"; +import { IERC4626 } from "interfaces/external/IERC4626.sol"; + +/// @title HarvesterVault +/// @author Angle Labs, Inc. +/// @dev Contract for anyone to permissionlessly adjust the reserves of Angle Transmuter through +/// the RebalancerFlashloanVault contract +contract HarvesterVault is BaseHarvester { + constructor( + address _rebalancer, + address vault, + uint64 targetExposure, + uint64 overrideExposures, + uint64 maxExposureYieldAsset, + uint64 minExposureYieldAsset, + uint96 _maxSlippage + ) + BaseHarvester( + _rebalancer, + address(IERC4626(vault).asset()), + vault, + targetExposure, + overrideExposures, + maxExposureYieldAsset, + minExposureYieldAsset, + _maxSlippage + ) + {} + + function setCollateralData( + address vault, + uint64 targetExposure, + uint64 minExposureYieldAsset, + uint64 maxExposureYieldAsset, + uint64 overrideExposures + ) public virtual onlyGuardian { + _setCollateralData( + address(IERC4626(vault).asset()), + vault, + targetExposure, + minExposureYieldAsset, + maxExposureYieldAsset, + overrideExposures + ); + } +} diff --git a/contracts/helpers/RebalancerFlashloanSwap.sol b/contracts/helpers/RebalancerFlashloanSwap.sol new file mode 100644 index 00000000..cfa19111 --- /dev/null +++ b/contracts/helpers/RebalancerFlashloanSwap.sol @@ -0,0 +1,94 @@ +// SPDX-License-Identifier: GPL-3.0 + +pragma solidity ^0.8.19; + +import "./BaseRebalancerFlashloan.sol"; +import { IERC20Metadata } from "oz/interfaces/IERC20Metadata.sol"; +import { RouterSwapper } from "utils/src/RouterSwapper.sol"; + +/// @title RebalancerFlashloanSwap +/// @author Angle Labs, Inc. +/// @dev Rebalancer contract for a Transmuter with as collaterals a liquid stablecoin and an yield bearing asset +/// using this liquid stablecoin as an asset +contract RebalancerFlashloanSwap is BaseRebalancerFlashloan, RouterSwapper { + using SafeCast for uint256; + + uint32 public maxSlippage; + + constructor( + IAccessControlManager _accessControlManager, + ITransmuter _transmuter, + IERC3156FlashLender _flashloan, + address _swapRouter, + address _tokenTransferAddress, + uint32 _maxSlippage + ) + BaseRebalancerFlashloan(_accessControlManager, _transmuter, _flashloan) + RouterSwapper(_swapRouter, _tokenTransferAddress) + { + maxSlippage = _maxSlippage; + } + + /** + * @notice Set the token transfer address + * @param newTokenTransferAddress address of the token transfer contract + */ + function setTokenTransferAddress(address newTokenTransferAddress) public override onlyGuardian { + super.setTokenTransferAddress(newTokenTransferAddress); + } + + /** + * @notice Set the swap router + * @param newSwapRouter address of the swap router + */ + function setSwapRouter(address newSwapRouter) public override onlyGuardian { + super.setSwapRouter(newSwapRouter); + } + + /** + * @notice Set the max slippage + * @param _maxSlippage max slippage in BPS + */ + function setMaxSlippage(uint32 _maxSlippage) external onlyGuardian { + maxSlippage = _maxSlippage; + } + + /** + * @notice Swap token using the router/aggregator + * @param tokenIn address of the token to swap + * @param tokenOut address of the token to receive + * @param amount amount of token to swap + * @param callData bytes to call the router/aggregator + */ + function _swapToTokenIn( + uint256, + address tokenIn, + address tokenOut, + uint256 amount, + bytes memory callData + ) internal override returns (uint256) { + uint256 balance = IERC20(tokenIn).balanceOf(address(this)); + + address[] memory tokens = new address[](1); + tokens[0] = tokenOut; + bytes[] memory callDatas = new bytes[](1); + callDatas[0] = callData; + uint256[] memory amounts = new uint256[](1); + amounts[0] = amount; + _swap(tokens, callDatas, amounts); + + uint256 amountOut = IERC20(tokenIn).balanceOf(address(this)) - balance; + uint256 decimalsTokenOut = IERC20Metadata(tokenOut).decimals(); + uint256 decimalsTokenIn = IERC20Metadata(tokenIn).decimals(); + + if (decimalsTokenOut > decimalsTokenIn) { + amount /= 10 ** (decimalsTokenOut - decimalsTokenIn); + } else if (decimalsTokenOut < decimalsTokenIn) { + amount *= 10 ** (decimalsTokenIn - decimalsTokenOut); + } + if (amountOut < (amount * (BPS - maxSlippage)) / BPS) { + revert SlippageTooHigh(); + } + return amountOut; + } +} diff --git a/contracts/helpers/RebalancerFlashloanVault.sol b/contracts/helpers/RebalancerFlashloanVault.sol new file mode 100644 index 00000000..1a696f46 --- /dev/null +++ b/contracts/helpers/RebalancerFlashloanVault.sol @@ -0,0 +1,41 @@ +// SPDX-License-Identifier: GPL-3.0 + +pragma solidity ^0.8.19; + +import "./BaseRebalancerFlashloan.sol"; +import { IERC4626 } from "interfaces/external/IERC4626.sol"; + +/// @title RebalancerFlashloanVault +/// @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 RebalancerFlashloanVault is BaseRebalancerFlashloan { + using SafeCast for uint256; + + constructor( + IAccessControlManager _accessControlManager, + ITransmuter _transmuter, + IERC3156FlashLender _flashloan + ) BaseRebalancerFlashloan(_accessControlManager, _transmuter, _flashloan) {} + + /** + * @dev Deposit or redeem the vault asset + * @param typeAction 1 for deposit, 2 for redeem + * @param tokenIn address of the token to swap + * @param tokenOut address of the token to receive + * @param amount amount of token to swap + */ + function _swapToTokenIn( + uint256 typeAction, + address tokenIn, + address tokenOut, + uint256 amount, + bytes memory + ) internal override returns (uint256 amountOut) { + if (typeAction == 1) { + // Granting allowance with the collateral for the vault asset + _adjustAllowance(tokenOut, tokenIn, amount); + amountOut = IERC4626(tokenIn).deposit(amount, address(this)); + } else amountOut = IERC4626(tokenOut).redeem(amount, address(this), address(this)); + } +} diff --git a/contracts/interfaces/IRebalancer.sol b/contracts/interfaces/IRebalancer.sol index ceab54f6..77e1b56b 100644 --- a/contracts/interfaces/IRebalancer.sol +++ b/contracts/interfaces/IRebalancer.sol @@ -2,6 +2,8 @@ pragma solidity >=0.5.0; +import "./ITransmuter.sol"; + struct Order { // Total agToken budget allocated to subsidize the swaps between the tokens associated to the order uint112 subsidyBudget; @@ -52,4 +54,6 @@ interface IRebalancer { /// @notice Recovers `amount` of `token` to the `to` address /// @dev This function checks if too much is not being recovered with respect to currently available budgets function recover(address token, uint256 amount, address to) external; + + function TRANSMUTER() external view returns (ITransmuter); } diff --git a/contracts/interfaces/IRebalancerFlashloan.sol b/contracts/interfaces/IRebalancerFlashloan.sol new file mode 100644 index 00000000..9d4e5fb7 --- /dev/null +++ b/contracts/interfaces/IRebalancerFlashloan.sol @@ -0,0 +1,24 @@ +// SPDX-License-Identifier: GPL-3.0 + +pragma solidity >=0.5.0; + +import "./IRebalancer.sol"; +import { IERC3156FlashBorrower } from "oz/interfaces/IERC3156FlashBorrower.sol"; + +/// @title IRebalancer +/// @author Angle Labs, Inc. +interface IRebalancerFlashloan is IRebalancer, IERC3156FlashBorrower { + /// @notice Burns `amountStablecoins` for one collateral asset and use the proceeds to mint the other collateral + /// asset + /// @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, swapping for the yield bearing asset, 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 asset, + uint256 minAmountOut, + bytes calldata extraData + ) external; +} diff --git a/contracts/utils/Errors.sol b/contracts/utils/Errors.sol index 3115946a..855c8099 100644 --- a/contracts/utils/Errors.sol +++ b/contracts/utils/Errors.sol @@ -42,3 +42,5 @@ error TooLate(); error TooSmallAmountOut(); error ZeroAddress(); error ZeroAmount(); +error SwapError(); +error SlippageTooHigh(); diff --git a/foundry.toml b/foundry.toml index 18a7be4d..66e0b4f2 100644 --- a/foundry.toml +++ b/foundry.toml @@ -42,17 +42,7 @@ base = "${ETH_NODE_URI_BASE}" linea = "${ETH_NODE_URI_LINEA}" [etherscan] -arbitrum = { key = "${ARBITRUM_ETHERSCAN_API_KEY}" } -gnosis = { key = "${GNOSIS_ETHERSCAN_API_KEY}" , url = "https://api.gnosisscan.io/api"} -mainnet = { key = "${MAINNET_ETHERSCAN_API_KEY}" } -optimism = { key = "${OPTIMISM_ETHERSCAN_API_KEY}" } -polygon = { key = "${POLYGON_ETHERSCAN_API_KEY}" } -avalanche = { key = "${AVALANCHE_ETHERSCAN_API_KEY}" } -celo = { key = "${CELO_ETHERSCAN_API_KEY}", url = "https://api.celoscan.io/api" } -base = { key = "${BASE_ETHERSCAN_API_KEY}", url = "https://api.basescan.org/api" } -polygon-zkevm = { key = "${POLYGONZKEVM_ETHERSCAN_API_KEY}", url = "https://api-zkevm.polygonscan.com/api" } -bsc = { key = "${BSC_ETHERSCAN_API_KEY}"} -linea = { key = "${LINEA_ETHERSCAN_API_KEY}"} + [profile.dev] optimizer = true diff --git a/lib/forge-std b/lib/forge-std index 9b49a72c..bf660614 160000 --- a/lib/forge-std +++ b/lib/forge-std @@ -1 +1 @@ -Subproject commit 9b49a72cfdb36bcf195eb863f868f01a6d6d3186 +Subproject commit bf6606142994b1e47e2882ce0cd477c020d77623 diff --git a/lib/utils b/lib/utils index c10de4a2..addaee4e 160000 --- a/lib/utils +++ b/lib/utils @@ -1 +1 @@ -Subproject commit c10de4a28622d21696ee8c50fbde8eb4fcd0279b +Subproject commit addaee4e6971172d44ca6739f3ef54d635734fc8 diff --git a/scripts/AdjustYieldExposure.s.sol b/scripts/AdjustYieldExposure.s.sol index d1c234db..b3c9e55d 100644 --- a/scripts/AdjustYieldExposure.s.sol +++ b/scripts/AdjustYieldExposure.s.sol @@ -7,7 +7,7 @@ import { stdJson } from "forge-std/StdJson.sol"; import "stringutils/strings.sol"; import "./Constants.s.sol"; -import { RebalancerFlashloan } from "contracts/helpers/RebalancerFlashloan.sol"; +import { RebalancerFlashloanVault } from "contracts/helpers/RebalancerFlashloanVault.sol"; contract AdjustYieldExposure is Utils { function run() external { @@ -17,8 +17,8 @@ contract AdjustYieldExposure is Utils { console.log(deployer.balance); vm.startBroadcast(deployerPrivateKey); - RebalancerFlashloan rebalancer = RebalancerFlashloan(0x22604C0E5633A9810E01c9cb469B23Eee17AC411); - rebalancer.adjustYieldExposure(1300000 * 1 ether, 0, USDC, STEAK_USDC, 1200000 * 1 ether); + RebalancerFlashloanVault rebalancer = RebalancerFlashloanVault(0x22604C0E5633A9810E01c9cb469B23Eee17AC411); + rebalancer.adjustYieldExposure(1300000 * 1 ether, 0, USDC, STEAK_USDC, 1200000 * 1 ether, new bytes(0)); vm.stopBroadcast(); } diff --git a/scripts/Constants.s.sol b/scripts/Constants.s.sol index 17593753..5700fb8d 100644 --- a/scripts/Constants.s.sol +++ b/scripts/Constants.s.sol @@ -23,6 +23,9 @@ address constant USDC = 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48; address constant BERNX = 0x3f95AA88dDbB7D9D484aa3D482bf0a80009c52c9; address constant STEAK_USDC = 0xBEEF01735c132Ada46AA9aA4c54623cAA92A64CB; address constant BIB01 = 0xCA30c93B02514f86d5C86a6e375E3A330B435Fb5; +address constant USDM = 0x59D9356E565Ab3A36dD77763Fc0d87fEaf85508C; + +address constant ONEINCH_ROUTER = 0x111111125421cA6dc452d289314280a0f8842A65; // EUROC uint128 constant FIREWALL_BURN_RATIO_EUROC = uint128(0); diff --git a/scripts/DeployHarvesterSwap.s.sol b/scripts/DeployHarvesterSwap.s.sol new file mode 100644 index 00000000..a6817db3 --- /dev/null +++ b/scripts/DeployHarvesterSwap.s.sol @@ -0,0 +1,36 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity ^0.8.19; + +import "./utils/Utils.s.sol"; +import { console } from "forge-std/console.sol"; +import { HarvesterSwap } from "contracts/helpers/HarvesterSwap.sol"; +import "./Constants.s.sol"; + +contract DeployHarvester 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); + address rebalancer = 0x22604C0E5633A9810E01c9cb469B23Eee17AC411; + address asset = USDM; + address collateral = USDC; + uint64 targetExposure = (13 * 1e9) / 100; + uint64 overrideExposures = 0; + uint96 maxSlippage = 1e9 / 100; + HarvesterSwap HarvesterSwap = new HarvesterSwap( + rebalancer, + collateral, + asset, + targetExposure, + overrideExposures, + 0, + 0, + maxSlippage + ); + console.log("HarvesterSwap deployed at: ", address(HarvesterSwap)); + + vm.stopBroadcast(); + } +} diff --git a/scripts/DeployHarvester.s.sol b/scripts/DeployHarvesterVault.s.sol similarity index 63% rename from scripts/DeployHarvester.s.sol rename to scripts/DeployHarvesterVault.s.sol index ea5a0a1f..085e567f 100644 --- a/scripts/DeployHarvester.s.sol +++ b/scripts/DeployHarvesterVault.s.sol @@ -3,10 +3,10 @@ pragma solidity ^0.8.19; import "./utils/Utils.s.sol"; import { console } from "forge-std/console.sol"; -import { Harvester } from "contracts/helpers/Harvester.sol"; +import { HarvesterVault } from "contracts/helpers/HarvesterVault.sol"; import "./Constants.s.sol"; -contract DeployHarvester is Utils { +contract DeployHarvesterVault is Utils { function run() external { uint256 deployerPrivateKey = vm.envUint("DEPLOYER_PRIVATE_KEY"); vm.startBroadcast(deployerPrivateKey); @@ -18,8 +18,16 @@ contract DeployHarvester is Utils { uint64 targetExposure = (13 * 1e9) / 100; uint64 overrideExposures = 0; uint96 maxSlippage = 1e9 / 100; - Harvester harvester = new Harvester(rebalancer, vault, targetExposure, overrideExposures, 0, 0, maxSlippage); - console.log("Harvester deployed at: ", address(harvester)); + HarvesterVault harvester = new HarvesterVault( + rebalancer, + vault, + targetExposure, + overrideExposures, + 0, + 0, + maxSlippage + ); + console.log("HarvesterVault deployed at: ", address(harvester)); vm.stopBroadcast(); } diff --git a/scripts/DeployRebalancerFlashloanSwap.s.sol b/scripts/DeployRebalancerFlashloanSwap.s.sol new file mode 100644 index 00000000..977cc7a3 --- /dev/null +++ b/scripts/DeployRebalancerFlashloanSwap.s.sol @@ -0,0 +1,36 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity ^0.8.19; + +import "./utils/Utils.s.sol"; +import { console } from "forge-std/console.sol"; +import { RebalancerFlashloanSwap } from "contracts/helpers/RebalancerFlashloanSwap.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 DeployRebalancerFlashloanSwapSwap 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)))); + RebalancerFlashloanSwap rebalancer = new RebalancerFlashloanSwap( + IAccessControlManager(_chainToContract(CHAIN_SOURCE, ContractType.CoreBorrow)), + ITransmuter(_chainToContract(CHAIN_SOURCE, ContractType.TransmuterAgUSD)), + IERC3156FlashLender(_chainToContract(CHAIN_SOURCE, ContractType.FlashLoan)), + ONEINCH_ROUTER, + ONEINCH_ROUTER, + 50 // 0.5% + ); + + console.log("Rebalancer deployed at: ", address(rebalancer)); + + vm.stopBroadcast(); + } +} diff --git a/scripts/DeployRebalancerFlashloan.s.sol b/scripts/DeployRebalancerFlashloanVault.s.sol similarity index 84% rename from scripts/DeployRebalancerFlashloan.s.sol rename to scripts/DeployRebalancerFlashloanVault.s.sol index 101718f7..1f1df245 100644 --- a/scripts/DeployRebalancerFlashloan.s.sol +++ b/scripts/DeployRebalancerFlashloanVault.s.sol @@ -3,7 +3,7 @@ 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 { RebalancerFlashloanVault } from "contracts/helpers/RebalancerFlashloanVault.sol"; import { IAccessControlManager } from "contracts/utils/AccessControl.sol"; import { ITransmuter } from "contracts/interfaces/ITransmuter.sol"; import { IERC3156FlashLender } from "oz/interfaces/IERC3156FlashLender.sol"; @@ -11,7 +11,7 @@ import "./Constants.s.sol"; import "oz/interfaces/IERC20.sol"; import "oz-upgradeable/token/ERC20/extensions/IERC20MetadataUpgradeable.sol"; -contract DeployRebalancerFlashloan is Utils { +contract DeployRebalancerFlashloanVault is Utils { function run() external { uint256 deployerPrivateKey = vm.envUint("DEPLOYER_PRIVATE_KEY"); vm.startBroadcast(deployerPrivateKey); @@ -20,13 +20,13 @@ contract DeployRebalancerFlashloan is Utils { 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( + RebalancerFlashloanVault rebalancer = new RebalancerFlashloanVault( IAccessControlManager(_chainToContract(CHAIN_SOURCE, ContractType.CoreBorrow)), ITransmuter(_chainToContract(CHAIN_SOURCE, ContractType.TransmuterAgUSD)), IERC3156FlashLender(0x4A2FF9bC686A0A23DA13B6194C69939189506F7F) ); /* - RebalancerFlashloan rebalancer = new RebalancerFlashloan( + RebalancerFlashloanVault rebalancer = new RebalancerFlashloanVault( IAccessControlManager(0x5bc6BEf80DA563EBf6Df6D6913513fa9A7ec89BE), ITransmuter(0x222222fD79264BBE280b4986F6FEfBC3524d0137), IERC3156FlashLender(0x4A2FF9bC686A0A23DA13B6194C69939189506F7F) diff --git a/scripts/Helpers.s.sol b/scripts/Helpers.s.sol index 51122e39..daa314a6 100644 --- a/scripts/Helpers.s.sol +++ b/scripts/Helpers.s.sol @@ -7,19 +7,6 @@ import "./utils/Utils.s.sol"; /// @title Utils /// @author Angle Labs, Inc. contract Helpers is Utils { - mapping(uint256 => uint256) internal forkIdentifier; - uint256 public arbitrumFork; - uint256 public avalancheFork; - uint256 public ethereumFork; - uint256 public optimismFork; - uint256 public polygonFork; - uint256 public gnosisFork; - uint256 public bnbFork; - uint256 public celoFork; - uint256 public polygonZkEVMFork; - uint256 public baseFork; - uint256 public lineaFork; - address public alice; address public bob; address public charlie; @@ -27,29 +14,7 @@ contract Helpers is Utils { address public sweeper; function setUp() public virtual { - arbitrumFork = vm.createFork(vm.envString("ETH_NODE_URI_ARBITRUM")); - avalancheFork = vm.createFork(vm.envString("ETH_NODE_URI_AVALANCHE")); - ethereumFork = vm.createFork(vm.envString("ETH_NODE_URI_MAINNET")); - optimismFork = vm.createFork(vm.envString("ETH_NODE_URI_OPTIMISM")); - polygonFork = vm.createFork(vm.envString("ETH_NODE_URI_POLYGON")); - gnosisFork = vm.createFork(vm.envString("ETH_NODE_URI_GNOSIS")); - bnbFork = vm.createFork(vm.envString("ETH_NODE_URI_BSC")); - celoFork = vm.createFork(vm.envString("ETH_NODE_URI_CELO")); - polygonZkEVMFork = vm.createFork(vm.envString("ETH_NODE_URI_POLYGON_ZKEVM")); - baseFork = vm.createFork(vm.envString("ETH_NODE_URI_BASE")); - lineaFork = vm.createFork(vm.envString("ETH_NODE_URI_LINEA")); - - forkIdentifier[CHAIN_ARBITRUM] = arbitrumFork; - forkIdentifier[CHAIN_AVALANCHE] = avalancheFork; - forkIdentifier[CHAIN_ETHEREUM] = ethereumFork; - forkIdentifier[CHAIN_OPTIMISM] = optimismFork; - forkIdentifier[CHAIN_POLYGON] = polygonFork; - forkIdentifier[CHAIN_GNOSIS] = gnosisFork; - forkIdentifier[CHAIN_BNB] = bnbFork; - forkIdentifier[CHAIN_CELO] = celoFork; - forkIdentifier[CHAIN_POLYGONZKEVM] = polygonZkEVMFork; - forkIdentifier[CHAIN_BASE] = baseFork; - forkIdentifier[CHAIN_LINEA] = lineaFork; + super.setUpForks(); alice = vm.addr(1); bob = vm.addr(2); diff --git a/scripts/test/UpdateTransmuterFacetsUSDATest.s.sol b/scripts/test/UpdateTransmuterFacetsUSDATest.s.sol index 5dc640da..ef360bef 100644 --- a/scripts/test/UpdateTransmuterFacetsUSDATest.s.sol +++ b/scripts/test/UpdateTransmuterFacetsUSDATest.s.sol @@ -53,8 +53,7 @@ contract UpdateTransmuterFacetsUSDATest is Helpers, Test { CHAIN_SOURCE = CHAIN_ETHEREUM; - ethereumFork = vm.createFork(vm.envString("ETH_NODE_URI_MAINNET"), 19483530); - vm.selectFork(ethereumFork); + ethereumFork = vm.createSelectFork("mainnet", 19499622); governor = DEPLOYER; transmuter = ITransmuter(0x222222fD79264BBE280b4986F6FEfBC3524d0137); @@ -689,7 +688,7 @@ contract UpdateTransmuterFacetsUSDATest is Helpers, Test { assertEq(ratio, BASE_18); } else if (redemption * BASE_18 < targetValue * (BASE_18 - firewallBurn)) { assertEq(mint, redemption); - assertEq(ratio, (redemption * BASE_18) / targetValue); + assertEq(ratio, BASE_18); } else { assertEq(mint, redemption); assertEq(ratio, BASE_18); diff --git a/test/fuzz/Harvester.t.sol b/test/fuzz/Harvester.t.sol index 6164649b..5bcb2e71 100644 --- a/test/fuzz/Harvester.t.sol +++ b/test/fuzz/Harvester.t.sol @@ -13,14 +13,14 @@ import "../utils/FunctionUtils.sol"; import "contracts/savings/Savings.sol"; import "../mock/MockTokenPermit.sol"; -import "contracts/helpers/RebalancerFlashloan.sol"; -import "contracts/helpers/Harvester.sol"; +import "contracts/helpers/RebalancerFlashloanVault.sol"; +import "contracts/helpers/HarvesterVault.sol"; contract HarvesterTest is Fixture, FunctionUtils { using SafeERC20 for IERC20; - RebalancerFlashloan public rebalancer; - Harvester public harvester; + RebalancerFlashloanVault public rebalancer; + HarvesterVault public harvester; Savings internal _saving; address internal _savingImplementation; string internal _name; @@ -50,8 +50,8 @@ contract HarvesterTest is Fixture, FunctionUtils { 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( + rebalancer = new RebalancerFlashloanVault(accessControlManager, transmuter, IERC3156FlashLender(governor)); + harvester = new HarvesterVault( address(rebalancer), address(_saving), targetExposure, @@ -76,7 +76,7 @@ contract HarvesterTest is Fixture, FunctionUtils { function test_Constructor_RevertWhen_InvalidParams() public { vm.expectRevert(); - new Harvester( + new HarvesterVault( address(0), address(_saving), targetExposure, @@ -87,7 +87,7 @@ contract HarvesterTest is Fixture, FunctionUtils { ); vm.expectRevert(); - new Harvester( + new HarvesterVault( address(rebalancer), address(0), targetExposure, @@ -98,7 +98,7 @@ contract HarvesterTest is Fixture, FunctionUtils { ); vm.expectRevert(Errors.InvalidParam.selector); - harvester = new Harvester( + harvester = new HarvesterVault( address(rebalancer), address(_saving), 1e10, @@ -109,7 +109,7 @@ contract HarvesterTest is Fixture, FunctionUtils { ); vm.expectRevert(Errors.InvalidParam.selector); - harvester = new Harvester( + harvester = new HarvesterVault( address(rebalancer), address(_saving), 1e10, @@ -120,13 +120,21 @@ contract HarvesterTest is Fixture, FunctionUtils { ); vm.expectRevert(Errors.InvalidParam.selector); - harvester = new Harvester(address(rebalancer), address(_saving), 1e9 / 10, 1, 1e10, minExposureYieldAsset, 1e8); + harvester = new HarvesterVault( + 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); + harvester = new HarvesterVault(address(rebalancer), address(_saving), 1e9 / 10, 1, 1e8, 2e8, 1e8); vm.expectRevert(Errors.InvalidParam.selector); - harvester = new Harvester( + harvester = new HarvesterVault( address(rebalancer), address(_saving), targetExposure, diff --git a/test/fuzz/RebalancerFlashloanTest.t.sol b/test/fuzz/RebalancerFlashloanVaultTest.t.sol similarity index 88% rename from test/fuzz/RebalancerFlashloanTest.t.sol rename to test/fuzz/RebalancerFlashloanVaultTest.t.sol index efd613d8..fe80f033 100644 --- a/test/fuzz/RebalancerFlashloanTest.t.sol +++ b/test/fuzz/RebalancerFlashloanVaultTest.t.sol @@ -13,12 +13,12 @@ import "../utils/FunctionUtils.sol"; import "contracts/savings/Savings.sol"; import "../mock/MockTokenPermit.sol"; -import "contracts/helpers/RebalancerFlashloan.sol"; +import "contracts/helpers/RebalancerFlashloanVault.sol"; -contract RebalancerFlashloanTest is Fixture, FunctionUtils { +contract RebalancerFlashloanVaultTest is Fixture, FunctionUtils { using SafeERC20 for IERC20; - RebalancerFlashloan public rebalancer; + RebalancerFlashloanVault public rebalancer; Savings internal _saving; string internal _name; string internal _symbol; @@ -42,7 +42,7 @@ contract RebalancerFlashloanTest is Fixture, FunctionUtils { _saving.initialize(accessControlManager, IERC20MetadataUpgradeable(address(token)), _name, _symbol, BASE_6); vm.stopPrank(); - rebalancer = new RebalancerFlashloan(accessControlManager, transmuter, IERC3156FlashLender(governor)); + rebalancer = new RebalancerFlashloanVault(accessControlManager, transmuter, IERC3156FlashLender(governor)); } function test_RebalancerInitialization() public { @@ -56,12 +56,12 @@ contract RebalancerFlashloanTest is Fixture, FunctionUtils { function test_Constructor_RevertWhen_ZeroAddress() public { vm.expectRevert(Errors.ZeroAddress.selector); - new RebalancerFlashloan(accessControlManager, transmuter, IERC3156FlashLender(address(0))); + new RebalancerFlashloanVault(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); + rebalancer.adjustYieldExposure(1, 1, address(0), address(0), 0, new bytes(0)); } function test_onFlashLoan_RevertWhen_NotTrusted() public { diff --git a/test/mock/MockRouter.sol b/test/mock/MockRouter.sol new file mode 100644 index 00000000..5ea8814f --- /dev/null +++ b/test/mock/MockRouter.sol @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: Unlicensed +pragma solidity ^0.8.19; + +import { SafeERC20, IERC20 } from "oz/token/ERC20/utils/SafeERC20.sol"; + +contract MockRouter { + using SafeERC20 for IERC20; + + function swap(uint256 amountIn, address tokenIn, uint256 amountOut, address tokenOut) external { + IERC20(tokenIn).safeTransferFrom(msg.sender, address(this), amountIn); + IERC20(tokenOut).safeTransfer(msg.sender, amountOut); + } +} diff --git a/test/scripts/HarvesterSwapUSDATest.t.sol b/test/scripts/HarvesterSwapUSDATest.t.sol new file mode 100644 index 00000000..5736a7a4 --- /dev/null +++ b/test/scripts/HarvesterSwapUSDATest.t.sol @@ -0,0 +1,136 @@ +// 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 "utils/src/CommonUtils.sol"; +import { IERC20 } from "oz/interfaces/IERC20.sol"; +import { IAgToken } from "interfaces/IAgToken.sol"; +import { HarvesterSwap } from "contracts/helpers/HarvesterSwap.sol"; +import { MockRouter } from "../mock/MockRouter.sol"; + +import { RebalancerFlashloanSwap, IERC3156FlashLender } from "contracts/helpers/RebalancerFlashloanSwap.sol"; + +interface IFlashAngle { + function addStablecoinSupport(address _treasury) external; + + function setFlashLoanParameters(address stablecoin, uint64 _flashLoanFee, uint256 _maxBorrowable) external; +} + +contract HarvesterSwapUSDATest is Test, CommonUtils { + using stdJson for string; + + ITransmuter transmuter; + IERC20 USDA; + IAgToken treasuryUSDA; + IFlashAngle FLASHLOAN; + address governor; + RebalancerFlashloanSwap public rebalancer; + MockRouter public router; + HarvesterSwap harvester; + uint64 public targetExposure; + uint64 public maxExposureYieldAsset; + uint64 public minExposureYieldAsset; + + address constant WHALE = 0x54D7aE423Edb07282645e740C046B9373970a168; + + function setUp() public { + ethereumFork = vm.createSelectFork(vm.envString("ETH_NODE_URI_MAINNET"), 20590478); + + transmuter = ITransmuter(_chainToContract(CHAIN_ETHEREUM, ContractType.TransmuterAgUSD)); + USDA = IERC20(_chainToContract(CHAIN_ETHEREUM, ContractType.AgUSD)); + FLASHLOAN = IFlashAngle(_chainToContract(CHAIN_ETHEREUM, ContractType.FlashLoan)); + treasuryUSDA = IAgToken(_chainToContract(CHAIN_ETHEREUM, ContractType.TreasuryAgUSD)); + governor = _chainToContract(CHAIN_ETHEREUM, ContractType.GovernorMultisig); + + // Setup rebalancer + router = new MockRouter(); + rebalancer = new RebalancerFlashloanSwap( + // Mock access control manager for USDA + IAccessControlManager(0x3fc5a1bd4d0A435c55374208A6A81535A1923039), + transmuter, + IERC3156FlashLender(address(FLASHLOAN)), + address(router), + address(router), + 50 + ); + targetExposure = uint64((15 * 1e9) / 100); + maxExposureYieldAsset = uint64((80 * 1e9) / 100); + minExposureYieldAsset = uint64((5 * 1e9) / 100); + + harvester = new HarvesterSwap( + address(rebalancer), + USDC, + USDM, + targetExposure, + 1, + maxExposureYieldAsset, + minExposureYieldAsset, + 1e8 + ); + + vm.startPrank(governor); + deal(address(USDA), address(rebalancer), BASE_18 * 1000); + rebalancer.setOrder(USDM, address(USDC), BASE_18 * 500, 0); + rebalancer.setOrder(address(USDC), USDM, BASE_18 * 500, 0); + transmuter.toggleWhitelist(Storage.WhitelistType.BACKED, NEW_DEPLOYER); + transmuter.toggleTrusted(governor, Storage.TrustedType.Seller); + transmuter.toggleTrusted(address(harvester), Storage.TrustedType.Seller); + + vm.stopPrank(); + + // Initialize Transmuter reserves + deal(USDC, NEW_DEPLOYER, 100000 * BASE_18); + vm.startPrank(NEW_DEPLOYER); + IERC20(USDC).approve(address(transmuter), type(uint256).max); + transmuter.swapExactOutput( + 1200 * 10 ** 21, + type(uint256).max, + USDC, + address(USDA), + NEW_DEPLOYER, + block.timestamp + ); + vm.stopPrank(); + vm.startPrank(WHALE); + IERC20(USDM).approve(address(transmuter), type(uint256).max); + transmuter.swapExactOutput(1200 * 10 ** 21, type(uint256).max, USDM, address(USDA), WHALE, block.timestamp); + vm.stopPrank(); + } + + function testUnit_Harvest_IncreaseUSDMExposure() external { + (uint256 fromUSDC, uint256 total) = transmuter.getIssuedByCollateral(address(USDC)); + (uint256 fromUSDM, ) = transmuter.getIssuedByCollateral(USDM); + + uint256 amount = 877221843438992898201107; + uint256 quoteAmount = transmuter.quoteIn(amount, address(USDA), USDC); + vm.prank(WHALE); + IERC20(USDM).transfer(address(router), quoteAmount * 1e12); + + bytes memory data = abi.encodeWithSelector( + MockRouter.swap.selector, + quoteAmount, + USDC, + quoteAmount * 1e12, + USDM + ); + harvester.harvest(USDC, 1e9, data); + (uint256 fromUSDC2, uint256 total2) = transmuter.getIssuedByCollateral(address(USDC)); + (uint256 fromUSDM2, ) = transmuter.getIssuedByCollateral(USDM); + assertGt(fromUSDC, fromUSDC2); + assertGt(fromUSDM2, fromUSDM); + assertGt(total, total2); + assertApproxEqRel((fromUSDC2 * 1e9) / total2, targetExposure, 100 * BPS); + assertApproxEqRel(fromUSDC2 * 1e9, targetExposure * total, 100 * BPS); + } +} diff --git a/test/scripts/HarvesterUSDATest.t.sol b/test/scripts/HarvesterUSDATest.t.sol index d665f3ac..13942147 100644 --- a/test/scripts/HarvesterUSDATest.t.sol +++ b/test/scripts/HarvesterUSDATest.t.sol @@ -15,9 +15,9 @@ 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 { HarvesterVault } from "contracts/helpers/HarvesterVault.sol"; -import { RebalancerFlashloan, IERC4626, IERC3156FlashLender } from "contracts/helpers/RebalancerFlashloan.sol"; +import { RebalancerFlashloanVault, IERC4626, IERC3156FlashLender } from "contracts/helpers/RebalancerFlashloanVault.sol"; interface IFlashAngle { function addStablecoinSupport(address _treasury) external; @@ -33,9 +33,9 @@ contract HarvesterUSDATest is Test { IAgToken treasuryUSDA; IFlashAngle FLASHLOAN; address governor; - RebalancerFlashloan public rebalancer; + RebalancerFlashloanVault public rebalancer; uint256 ethereumFork; - Harvester harvester; + HarvesterVault harvester; uint64 public targetExposure; uint64 public maxExposureYieldAsset; uint64 public minExposureYieldAsset; @@ -48,12 +48,18 @@ contract HarvesterUSDATest is Test { FLASHLOAN = IFlashAngle(0x4A2FF9bC686A0A23DA13B6194C69939189506F7F); treasuryUSDA = IAgToken(0xf8588520E760BB0b3bDD62Ecb25186A28b0830ee); governor = 0xdC4e6DFe07EFCa50a197DF15D9200883eF4Eb1c8; - rebalancer = RebalancerFlashloan(0x22604C0E5633A9810E01c9cb469B23Eee17AC411); + // Setup rebalancer + rebalancer = new RebalancerFlashloanVault( + // Mock access control manager for USDA + IAccessControlManager(0x3fc5a1bd4d0A435c55374208A6A81535A1923039), + transmuter, + IERC3156FlashLender(address(FLASHLOAN)) + ); targetExposure = uint64((15 * 1e9) / 100); maxExposureYieldAsset = uint64((80 * 1e9) / 100); minExposureYieldAsset = uint64((5 * 1e9) / 100); - harvester = new Harvester( + harvester = new HarvesterVault( address(rebalancer), address(STEAK_USDC), targetExposure, @@ -64,6 +70,9 @@ contract HarvesterUSDATest is Test { ); vm.startPrank(governor); + deal(address(USDA), address(rebalancer), BASE_18 * 1000); + rebalancer.setOrder(address(STEAK_USDC), address(USDC), BASE_18 * 500, 0); + rebalancer.setOrder(address(USDC), address(STEAK_USDC), BASE_18 * 500, 0); transmuter.toggleWhitelist(Storage.WhitelistType.BACKED, NEW_DEPLOYER); transmuter.toggleTrusted(governor, Storage.TrustedType.Seller); transmuter.toggleTrusted(address(harvester), Storage.TrustedType.Seller); @@ -75,7 +84,7 @@ contract HarvesterUSDATest is Test { // 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); + harvester.harvest(USDC, 1e9, new bytes(0)); (uint256 fromUSDC2, uint256 total2) = transmuter.getIssuedByCollateral(address(USDC)); (uint256 fromSTEAK2, ) = transmuter.getIssuedByCollateral(address(STEAK_USDC)); assertGt(fromUSDC2, fromUSDC); @@ -84,7 +93,7 @@ contract HarvesterUSDATest is Test { assertApproxEqRel((fromUSDC2 * 1e9) / total2, targetExposure, 100 * BPS); assertApproxEqRel(fromUSDC2 * 1e9, targetExposure * total, 100 * BPS); - harvester.harvest(USDC); + harvester.harvest(USDC, 1e9, new bytes(0)); (uint256 fromUSDC3, uint256 total3) = transmuter.getIssuedByCollateral(address(USDC)); (uint256 fromSTEAK3, ) = transmuter.getIssuedByCollateral(address(STEAK_USDC)); assertGt(fromUSDC3, fromUSDC2); @@ -103,14 +112,14 @@ contract HarvesterUSDATest is Test { vm.stopPrank(); (uint256 fromUSDC, uint256 total) = transmuter.getIssuedByCollateral(address(USDC)); (uint256 fromSTEAK, ) = transmuter.getIssuedByCollateral(address(STEAK_USDC)); - harvester.harvest(USDC); + harvester.harvest(USDC, 1e9, new bytes(0)); (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); + harvester.harvest(USDC, 1e9, new bytes(0)); (uint256 fromUSDC3, uint256 total3) = transmuter.getIssuedByCollateral(address(USDC)); (uint256 fromSTEAK3, ) = transmuter.getIssuedByCollateral(address(STEAK_USDC)); assertEq(fromUSDC3, fromUSDC2); @@ -127,7 +136,7 @@ contract HarvesterUSDATest is Test { (uint256 fromUSDC, uint256 total) = transmuter.getIssuedByCollateral(address(USDC)); (uint256 fromSTEAK, ) = transmuter.getIssuedByCollateral(address(STEAK_USDC)); - harvester.harvest(USDC); + harvester.harvest(USDC, 1e9, new bytes(0)); (uint256 fromUSDC2, uint256 total2) = transmuter.getIssuedByCollateral(address(USDC)); (uint256 fromSTEAK2, ) = transmuter.getIssuedByCollateral(address(STEAK_USDC)); assertGt(fromUSDC2, fromUSDC); @@ -135,7 +144,7 @@ contract HarvesterUSDATest is Test { assertGt(total, total2); assertApproxEqRel((fromSTEAK2 * 1e9) / total2, (73 * 1e9) / 100, 100 * BPS); - harvester.harvest(USDC); + harvester.harvest(USDC, 1e9, new bytes(0)); (uint256 fromUSDC3, uint256 total3) = transmuter.getIssuedByCollateral(address(USDC)); (uint256 fromSTEAK3, ) = transmuter.getIssuedByCollateral(address(STEAK_USDC)); assertGt(fromUSDC3, fromUSDC2); @@ -153,7 +162,7 @@ contract HarvesterUSDATest is Test { (uint256 fromUSDC, uint256 total) = transmuter.getIssuedByCollateral(address(USDC)); (uint256 fromSTEAK, ) = transmuter.getIssuedByCollateral(address(STEAK_USDC)); - harvester.harvest(USDC); + harvester.harvest(USDC, 1e9, new bytes(0)); (uint256 fromUSDC2, uint256 total2) = transmuter.getIssuedByCollateral(address(USDC)); (uint256 fromSTEAK2, ) = transmuter.getIssuedByCollateral(address(STEAK_USDC)); @@ -162,7 +171,7 @@ contract HarvesterUSDATest is Test { assertGt(total, total2); assertApproxEqRel((fromUSDC2 * 1e9) / total2, (5 * 1e9) / 100, 100 * BPS); - harvester.harvest(USDC); + harvester.harvest(USDC, 1e9, new bytes(0)); (uint256 fromUSDC3, uint256 total3) = transmuter.getIssuedByCollateral(address(USDC)); (uint256 fromSTEAK3, ) = transmuter.getIssuedByCollateral(address(STEAK_USDC)); assertGt(fromUSDC2, fromUSDC3); @@ -181,7 +190,7 @@ contract HarvesterUSDATest is Test { (uint256 fromUSDC, uint256 total) = transmuter.getIssuedByCollateral(address(USDC)); (uint256 fromSTEAK, ) = transmuter.getIssuedByCollateral(address(STEAK_USDC)); - harvester.harvest(USDC); + harvester.harvest(USDC, 1e9, new bytes(0)); (uint256 fromUSDC2, uint256 total2) = transmuter.getIssuedByCollateral(address(USDC)); (uint256 fromSTEAK2, ) = transmuter.getIssuedByCollateral(address(STEAK_USDC)); @@ -189,7 +198,7 @@ contract HarvesterUSDATest is Test { assertEq(fromSTEAK2, fromSTEAK); assertEq(total, total2); - harvester.harvest(USDC); + harvester.harvest(USDC, 1e9, new bytes(0)); (uint256 fromUSDC3, uint256 total3) = transmuter.getIssuedByCollateral(address(USDC)); (uint256 fromSTEAK3, ) = transmuter.getIssuedByCollateral(address(STEAK_USDC)); assertEq(fromUSDC2, fromUSDC3); @@ -206,7 +215,7 @@ contract HarvesterUSDATest is Test { (uint256 fromUSDC, uint256 total) = transmuter.getIssuedByCollateral(address(USDC)); (uint256 fromSTEAK, ) = transmuter.getIssuedByCollateral(address(STEAK_USDC)); - harvester.harvest(USDC); + harvester.harvest(USDC, 1e9, new bytes(0)); (uint256 fromUSDC2, uint256 total2) = transmuter.getIssuedByCollateral(address(USDC)); (uint256 fromSTEAK2, ) = transmuter.getIssuedByCollateral(address(STEAK_USDC)); @@ -216,7 +225,7 @@ contract HarvesterUSDATest is Test { assertLe((fromSTEAK2 * 1e9) / total2, (755 * 1e9) / 1000); assertApproxEqRel((fromSTEAK2 * 1e9) / total2, (755 * 1e9) / 1000, 100 * BPS); - harvester.harvest(USDC); + harvester.harvest(USDC, 1e9, new bytes(0)); (uint256 fromUSDC3, uint256 total3) = transmuter.getIssuedByCollateral(address(USDC)); (uint256 fromSTEAK3, ) = transmuter.getIssuedByCollateral(address(STEAK_USDC)); diff --git a/test/scripts/RebalancerSwapUSDATest.t.sol b/test/scripts/RebalancerSwapUSDATest.t.sol new file mode 100644 index 00000000..dd5bede4 --- /dev/null +++ b/test/scripts/RebalancerSwapUSDATest.t.sol @@ -0,0 +1,452 @@ +// 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 "utils/src/CommonUtils.sol"; +import { IERC20 } from "oz/interfaces/IERC20.sol"; +import { IAgToken } from "interfaces/IAgToken.sol"; +import { MockRouter } from "../mock/MockRouter.sol"; + +import { RebalancerFlashloanSwap, IERC3156FlashLender } from "contracts/helpers/RebalancerFlashloanSwap.sol"; + +interface IFlashAngle { + function addStablecoinSupport(address _treasury) external; + + function setFlashLoanParameters(address stablecoin, uint64 _flashLoanFee, uint256 _maxBorrowable) external; +} + +contract RebalancerSwapUSDATest is Test, CommonUtils { + using stdJson for string; + + ITransmuter transmuter; + IERC20 USDA; + IAgToken treasuryUSDA; + IFlashAngle FLASHLOAN; + address governor; + RebalancerFlashloanSwap public rebalancer; + MockRouter public router; + + address constant WHALE = 0x54D7aE423Edb07282645e740C046B9373970a168; + + function setUp() public { + ethereumFork = vm.createSelectFork(vm.envString("ETH_NODE_URI_MAINNET"), 20590478); + + transmuter = ITransmuter(_chainToContract(CHAIN_ETHEREUM, ContractType.TransmuterAgUSD)); + USDA = IERC20(_chainToContract(CHAIN_ETHEREUM, ContractType.AgUSD)); + FLASHLOAN = IFlashAngle(_chainToContract(CHAIN_ETHEREUM, ContractType.FlashLoan)); + treasuryUSDA = IAgToken(_chainToContract(CHAIN_ETHEREUM, ContractType.TreasuryAgUSD)); + governor = _chainToContract(CHAIN_ETHEREUM, ContractType.GovernorMultisig); + + 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 + router = new MockRouter(); + rebalancer = new RebalancerFlashloanSwap( + // Mock access control manager for USDA + IAccessControlManager(0x3fc5a1bd4d0A435c55374208A6A81535A1923039), + transmuter, + IERC3156FlashLender(address(FLASHLOAN)), + address(router), + address(router), + 50 + ); + + // 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(USDC, NEW_DEPLOYER, 100000 * BASE_18); + vm.startPrank(NEW_DEPLOYER); + IERC20(USDC).approve(address(transmuter), type(uint256).max); + transmuter.swapExactOutput( + 1200 * 10 ** 21, + type(uint256).max, + USDC, + address(USDA), + NEW_DEPLOYER, + block.timestamp + ); + vm.stopPrank(); + vm.startPrank(WHALE); + IERC20(USDM).approve(address(transmuter), type(uint256).max); + transmuter.swapExactOutput(1200 * 10 ** 21, type(uint256).max, USDM, address(USDA), WHALE, block.timestamp); + vm.stopPrank(); + } + + /*////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + GETTERS + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////*/ + + function testUnit_RebalancerSetup() external { + assertEq(address(transmuter.agToken()), 0x0000206329b97DB379d5E1Bf586BbDB969C63274); + // Revert when no order has been setup + vm.startPrank(governor); + vm.expectRevert(); + rebalancer.adjustYieldExposure(BASE_18, 1, USDC, USDM, 0, new bytes(0)); + + vm.expectRevert(); + rebalancer.adjustYieldExposure(BASE_18, 0, USDC, USDM, 0, new bytes(0)); + vm.stopPrank(); + } + + function testFuzz_adjustYieldExposure_SuccessIncrease(uint256 amount) external { + amount = bound(amount, BASE_18, BASE_18 * 100); + + uint256 quoteAmount = transmuter.quoteIn(amount, address(USDA), USDC); + vm.prank(WHALE); + IERC20(USDM).transfer(address(router), amount); + deal(address(USDA), address(rebalancer), BASE_18 * 1000); + + (uint256 fromUSDC, uint256 total) = transmuter.getIssuedByCollateral(address(USDC)); + (uint256 fromUSDM, ) = transmuter.getIssuedByCollateral(address(USDM)); + + vm.startPrank(governor); + rebalancer.setOrder(address(USDM), address(USDC), BASE_18 * 500, 0); + rebalancer.setOrder(address(USDC), address(USDM), BASE_18 * 500, 0); + + uint256 budget = rebalancer.budget(); + (uint112 orderBudget0, , , ) = rebalancer.orders(address(USDC), address(USDM)); + (uint112 orderBudget1, , , ) = rebalancer.orders(address(USDM), address(USDC)); + + bytes memory data = abi.encodeWithSelector( + MockRouter.swap.selector, + quoteAmount, + USDC, + quoteAmount * 1e12, + USDM + ); + rebalancer.adjustYieldExposure(amount, 1, USDC, USDM, 0, data); + + vm.stopPrank(); + + (uint256 fromUSDCPost, uint256 totalPost) = transmuter.getIssuedByCollateral(address(USDC)); + (uint256 fromUSDMPost, ) = transmuter.getIssuedByCollateral(address(USDM)); + assertLe(totalPost, total); + assertEq(fromUSDCPost, fromUSDC - amount); + assertLe(fromUSDMPost, fromUSDM + amount); + assertGe(fromUSDMPost, fromUSDM); + assertLe(rebalancer.budget(), budget); + (uint112 newOrder0, , , ) = rebalancer.orders(address(USDC), address(USDM)); + (uint112 newOrder1, , , ) = rebalancer.orders(address(USDM), address(USDC)); + assertEq(newOrder0, orderBudget0); + assertLe(newOrder1, orderBudget1); + } + + function testFuzz_adjustYieldExposure_RevertMinAmountOut(uint256 amount) external { + amount = bound(amount, BASE_18, BASE_18 * 100); + + uint256 quoteAmount = transmuter.quoteIn(amount, address(USDA), USDC); + vm.prank(WHALE); + IERC20(USDM).transfer(address(router), amount); + + deal(address(USDA), address(rebalancer), BASE_18 * 1000); + vm.startPrank(governor); + rebalancer.setOrder(address(USDM), address(USDC), BASE_18 * 500, 0); + rebalancer.setOrder(address(USDC), address(USDM), BASE_18 * 500, 0); + + bytes memory data = abi.encodeWithSelector( + MockRouter.swap.selector, + quoteAmount, + USDC, + quoteAmount * 1e12, + USDM + ); + vm.expectRevert(); + rebalancer.adjustYieldExposure(amount, 1, USDC, USDM, type(uint256).max, data); + + vm.stopPrank(); + } + + function testFuzz_adjustYieldExposure_SuccessDecrease(uint256 amount) external { + amount = bound(amount, BASE_18, BASE_18 * 100); + + uint256 quoteAmount = transmuter.quoteIn(amount, address(USDA), USDM); + deal(USDC, address(router), amount); + + deal(address(USDA), address(rebalancer), BASE_18 * 1000); + (uint256 fromUSDC, uint256 total) = transmuter.getIssuedByCollateral(address(USDC)); + (uint256 fromUSDM, ) = transmuter.getIssuedByCollateral(address(USDM)); + + vm.startPrank(governor); + rebalancer.setOrder(address(USDM), address(USDC), BASE_18 * 500, 0); + rebalancer.setOrder(address(USDC), address(USDM), BASE_18 * 500, 0); + uint256 budget = rebalancer.budget(); + (uint112 orderBudget0, , , ) = rebalancer.orders(address(USDC), address(USDM)); + (uint112 orderBudget1, , , ) = rebalancer.orders(address(USDM), address(USDC)); + + bytes memory data = abi.encodeWithSelector( + MockRouter.swap.selector, + quoteAmount, + USDM, + quoteAmount / 1e12, + USDC + ); + rebalancer.adjustYieldExposure(amount, 0, USDC, USDM, 0, data); + vm.stopPrank(); + + (uint256 fromUSDCPost, uint256 totalPost) = transmuter.getIssuedByCollateral(address(USDC)); + (uint256 fromUSDMPost, ) = transmuter.getIssuedByCollateral(address(USDM)); + assertLe(totalPost, total); + assertLe(fromUSDCPost, fromUSDC + amount); + assertGe(fromUSDCPost, fromUSDC); + assertEq(fromUSDMPost, fromUSDM - amount); + assertLe(rebalancer.budget(), budget); + (uint112 newOrder0, , , ) = rebalancer.orders(address(USDC), address(USDM)); + (uint112 newOrder1, , , ) = rebalancer.orders(address(USDM), address(USDC)); + assertLe(newOrder0, orderBudget0); + assertEq(newOrder1, orderBudget1); + } + + function testFuzz_adjustYieldExposure_TooHighSlipage(uint256 amount) external { + amount = bound(amount, BASE_18, BASE_18 * 100); + + uint256 quoteAmount = transmuter.quoteIn(amount, address(USDA), USDC); + vm.prank(WHALE); + IERC20(USDM).transfer(address(router), amount); + deal(address(USDA), address(rebalancer), BASE_18 * 1000); + + (uint256 fromUSDC, uint256 total) = transmuter.getIssuedByCollateral(address(USDC)); + (uint256 fromUSDM, ) = transmuter.getIssuedByCollateral(address(USDM)); + + vm.startPrank(governor); + rebalancer.setOrder(address(USDM), address(USDC), BASE_18 * 500, 0); + rebalancer.setOrder(address(USDC), address(USDM), BASE_18 * 500, 0); + + uint256 budget = rebalancer.budget(); + (uint112 orderBudget0, , , ) = rebalancer.orders(address(USDC), address(USDM)); + (uint112 orderBudget1, , , ) = rebalancer.orders(address(USDM), address(USDC)); + + bytes memory data = abi.encodeWithSelector( + MockRouter.swap.selector, + quoteAmount, + USDC, + quoteAmount - ((quoteAmount * rebalancer.maxSlippage()) / BPS) * 1e12, + USDM + ); + vm.expectRevert(); + rebalancer.adjustYieldExposure(amount, 1, USDC, USDM, 0, data); + + vm.stopPrank(); + } + + function testFuzz_adjustYieldExposure_SuccessDecreaseSplit(uint256 amount, uint256 split) external { + amount = bound(amount, BASE_18, BASE_18 * 100); + + deal(USDC, address(router), amount); + + 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 fromUSDM, ) = transmuter.getIssuedByCollateral(address(USDM)); + vm.startPrank(governor); + rebalancer.setOrder(address(USDM), address(USDC), BASE_18 * 500, 0); + rebalancer.setOrder(address(USDC), address(USDM), BASE_18 * 500, 0); + uint256 budget = rebalancer.budget(); + (uint112 orderBudget0, , , ) = rebalancer.orders(address(USDC), address(USDM)); + (uint112 orderBudget1, , , ) = rebalancer.orders(address(USDM), address(USDC)); + + { + uint256 quoteAmount = transmuter.quoteIn((amount * split) / BASE_9, address(USDA), USDM); + bytes memory data = abi.encodeWithSelector( + MockRouter.swap.selector, + quoteAmount, + USDM, + quoteAmount / 1e12, + USDC + ); + rebalancer.adjustYieldExposure((amount * split) / BASE_9, 0, USDC, USDM, 0, data); + } + + (uint256 fromUSDCPost, uint256 totalPost) = transmuter.getIssuedByCollateral(address(USDC)); + (uint256 fromUSDMPost, ) = transmuter.getIssuedByCollateral(address(USDM)); + assertLe(totalPost, total); + assertLe(fromUSDCPost, fromUSDC + (amount * split) / BASE_9); + assertGe(fromUSDCPost, fromUSDC); + assertEq(fromUSDMPost, fromUSDM - (amount * split) / BASE_9); + assertLe(rebalancer.budget(), budget); + + { + uint256 finalAmount = amount - (amount * split) / BASE_9; + uint256 quoteAmount = transmuter.quoteIn(finalAmount, address(USDA), USDM); + bytes memory data = abi.encodeWithSelector( + MockRouter.swap.selector, + quoteAmount, + USDM, + quoteAmount / 1e12, + USDC + ); + rebalancer.adjustYieldExposure(finalAmount, 0, USDC, USDM, 0, data); + } + + (fromUSDCPost, totalPost) = transmuter.getIssuedByCollateral(address(USDC)); + (fromUSDMPost, ) = transmuter.getIssuedByCollateral(address(USDM)); + assertLe(totalPost, total); + assertLe(fromUSDCPost, fromUSDC + amount); + assertGe(fromUSDCPost, fromUSDC); + assertEq(fromUSDMPost, fromUSDM - amount); + assertLe(rebalancer.budget(), budget); + (uint112 newOrder0, , , ) = rebalancer.orders(address(USDC), address(USDM)); + (uint112 newOrder1, , , ) = rebalancer.orders(address(USDM), 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); + + vm.prank(WHALE); + IERC20(USDM).transfer(address(router), amount); + + 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 fromUSDM, ) = transmuter.getIssuedByCollateral(address(USDM)); + vm.startPrank(governor); + rebalancer.setOrder(address(USDM), address(USDC), BASE_18 * 500, 0); + rebalancer.setOrder(address(USDC), address(USDM), BASE_18 * 500, 0); + uint256 budget = rebalancer.budget(); + (uint112 orderBudget0, , , ) = rebalancer.orders(address(USDC), address(USDM)); + (uint112 orderBudget1, , , ) = rebalancer.orders(address(USDM), address(USDC)); + + { + uint256 quoteAmount = transmuter.quoteIn((amount * split) / BASE_9, address(USDA), USDC); + bytes memory data = abi.encodeWithSelector( + MockRouter.swap.selector, + quoteAmount, + USDC, + quoteAmount * 1e12, + USDM + ); + rebalancer.adjustYieldExposure((amount * split) / BASE_9, 1, USDC, USDM, 0, data); + } + + (uint256 fromUSDCPost, uint256 totalPost) = transmuter.getIssuedByCollateral(address(USDC)); + (uint256 fromUSDMPost, ) = transmuter.getIssuedByCollateral(address(USDM)); + assertLe(totalPost, total); + assertEq(fromUSDCPost, fromUSDC - (amount * split) / BASE_9); + assertLe(fromUSDMPost, fromUSDM + (amount * split) / BASE_9); + assertGe(fromUSDMPost, fromUSDM); + assertLe(rebalancer.budget(), budget); + + { + uint256 finalAmount = amount - (amount * split) / BASE_9; + uint256 quoteAmount = transmuter.quoteIn(finalAmount, address(USDA), USDC); + bytes memory data = abi.encodeWithSelector( + MockRouter.swap.selector, + quoteAmount, + USDC, + quoteAmount * 1e12, + USDM + ); + rebalancer.adjustYieldExposure(finalAmount, 1, USDC, USDM, 0, data); + } + + (fromUSDCPost, totalPost) = transmuter.getIssuedByCollateral(address(USDC)); + (fromUSDMPost, ) = transmuter.getIssuedByCollateral(address(USDM)); + assertLe(totalPost, total); + assertEq(fromUSDCPost, fromUSDC - amount); + assertLe(fromUSDMPost, fromUSDM + amount); + assertGe(fromUSDMPost, fromUSDM); + assertLe(rebalancer.budget(), budget); + (uint112 newOrder0, , , ) = rebalancer.orders(address(USDC), address(USDM)); + (uint112 newOrder1, , , ) = rebalancer.orders(address(USDM), 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); + + vm.prank(WHALE); + IERC20(USDM).transfer(address(router), amount); + + deal(address(USDA), address(rebalancer), BASE_18 * 1000); + (uint256 fromUSDC, uint256 total) = transmuter.getIssuedByCollateral(address(USDC)); + (uint256 fromUSDM, ) = transmuter.getIssuedByCollateral(address(USDM)); + vm.startPrank(governor); + rebalancer.setOrder(address(USDM), address(USDC), BASE_18 * 500, 0); + rebalancer.setOrder(address(USDC), address(USDM), BASE_18 * 500, 0); + uint256 budget = rebalancer.budget(); + (uint112 orderBudget0, , , ) = rebalancer.orders(address(USDC), address(USDM)); + (uint112 orderBudget1, , , ) = rebalancer.orders(address(USDM), address(USDC)); + + { + uint256 quoteAmount = transmuter.quoteIn(amount, address(USDA), USDC); + bytes memory data = abi.encodeWithSelector( + MockRouter.swap.selector, + quoteAmount, + USDC, + quoteAmount * 1e12, + USDM + ); + rebalancer.adjustYieldExposure(amount, 1, USDC, USDM, 0, data); + } + + (uint256 fromUSDCPost, uint256 totalPost) = transmuter.getIssuedByCollateral(address(USDC)); + (uint256 fromUSDMPost, ) = transmuter.getIssuedByCollateral(address(USDM)); + assertLe(totalPost, total); + assertEq(fromUSDCPost, fromUSDC - amount); + assertLe(fromUSDMPost, fromUSDM + amount); + assertGe(fromUSDMPost, fromUSDM); + assertLe(rebalancer.budget(), budget); + (uint112 newOrder0, , , ) = rebalancer.orders(address(USDC), address(USDM)); + (uint112 newOrder1, , , ) = rebalancer.orders(address(USDM), address(USDC)); + assertEq(newOrder0, orderBudget0); + assertLe(newOrder1, orderBudget1); + + { + uint256 quoteAmount = transmuter.quoteIn(amount, address(USDA), USDM); + bytes memory data = abi.encodeWithSelector( + MockRouter.swap.selector, + quoteAmount, + USDM, + quoteAmount / 1e12, + USDC + ); + rebalancer.adjustYieldExposure(amount, 0, USDC, USDM, 0, data); + } + + (orderBudget0, , , ) = rebalancer.orders(address(USDC), address(USDM)); + (orderBudget1, , , ) = rebalancer.orders(address(USDM), address(USDC)); + assertLe(orderBudget0, newOrder0); + assertEq(orderBudget1, newOrder1); + + (uint256 fromUSDCPost2, uint256 totalPost2) = transmuter.getIssuedByCollateral(address(USDC)); + (uint256 fromUSDMPost2, ) = transmuter.getIssuedByCollateral(address(USDM)); + + assertLe(totalPost2, totalPost); + assertLe(fromUSDCPost2, fromUSDC); + assertLe(fromUSDMPost2, fromUSDM); + assertLe(fromUSDMPost2, fromUSDMPost); + assertGe(fromUSDCPost2, fromUSDCPost); + assertLe(rebalancer.budget(), budget); + + vm.stopPrank(); + } +} diff --git a/test/scripts/RebalancerUSDATest.t.sol b/test/scripts/RebalancerUSDATest.t.sol index aadc7d55..65de47f9 100644 --- a/test/scripts/RebalancerUSDATest.t.sol +++ b/test/scripts/RebalancerUSDATest.t.sol @@ -16,7 +16,7 @@ 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"; +import { RebalancerFlashloanVault, IERC4626, IERC3156FlashLender } from "contracts/helpers/RebalancerFlashloanVault.sol"; interface IFlashAngle { function addStablecoinSupport(address _treasury) external; @@ -32,7 +32,7 @@ contract RebalancerUSDATest is Test { IAgToken treasuryUSDA; IFlashAngle FLASHLOAN; address governor; - RebalancerFlashloan public rebalancer; + RebalancerFlashloanVault public rebalancer; uint256 ethereumFork; function setUp() public { @@ -51,7 +51,7 @@ contract RebalancerUSDATest is Test { vm.stopPrank(); // Setup rebalancer - rebalancer = new RebalancerFlashloan( + rebalancer = new RebalancerFlashloanVault( // Mock access control manager for USDA IAccessControlManager(0x3fc5a1bd4d0A435c55374208A6A81535A1923039), transmuter, @@ -92,10 +92,10 @@ contract RebalancerUSDATest is Test { // Revert when no order has been setup vm.startPrank(NEW_DEPLOYER); vm.expectRevert(); - rebalancer.adjustYieldExposure(BASE_18, 1, USDC, STEAK_USDC, 0); + rebalancer.adjustYieldExposure(BASE_18, 1, USDC, STEAK_USDC, 0, new bytes(0)); vm.expectRevert(); - rebalancer.adjustYieldExposure(BASE_18, 0, USDC, STEAK_USDC, 0); + rebalancer.adjustYieldExposure(BASE_18, 0, USDC, STEAK_USDC, 0, new bytes(0)); vm.stopPrank(); } @@ -111,7 +111,7 @@ contract RebalancerUSDATest is Test { 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); + rebalancer.adjustYieldExposure(amount, 1, USDC, STEAK_USDC, 0, new bytes(0)); vm.stopPrank(); @@ -135,7 +135,7 @@ contract RebalancerUSDATest is Test { 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); + rebalancer.adjustYieldExposure(amount, 1, USDC, STEAK_USDC, type(uint256).max, new bytes(0)); vm.stopPrank(); } @@ -150,7 +150,7 @@ contract RebalancerUSDATest is Test { 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); + rebalancer.adjustYieldExposure(amount, 0, USDC, STEAK_USDC, 0, new bytes(0)); vm.stopPrank(); (uint256 fromUSDCPost, uint256 totalPost) = transmuter.getIssuedByCollateral(address(USDC)); (uint256 fromSTEAKPost, ) = transmuter.getIssuedByCollateral(address(STEAK_USDC)); @@ -185,7 +185,7 @@ contract RebalancerUSDATest is Test { transmuter.setFees(STEAK_USDC, xMintFee, yMintFee, true); transmuter.updateOracle(STEAK_USDC); - rebalancer.adjustYieldExposure(amount, 1, USDC, STEAK_USDC, 0); + rebalancer.adjustYieldExposure(amount, 1, USDC, STEAK_USDC, 0, new bytes(0)); vm.stopPrank(); (uint256 fromUSDCPost, uint256 totalPost) = transmuter.getIssuedByCollateral(address(USDC)); (uint256 fromSTEAKPost, ) = transmuter.getIssuedByCollateral(address(STEAK_USDC)); @@ -207,7 +207,7 @@ contract RebalancerUSDATest is Test { (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); + rebalancer.adjustYieldExposure((amount * split) / BASE_9, 0, USDC, STEAK_USDC, 0, new bytes(0)); (uint256 fromUSDCPost, uint256 totalPost) = transmuter.getIssuedByCollateral(address(USDC)); (uint256 fromSTEAKPost, ) = transmuter.getIssuedByCollateral(address(STEAK_USDC)); @@ -217,7 +217,7 @@ contract RebalancerUSDATest is Test { assertEq(fromSTEAKPost, fromSTEAK - (amount * split) / BASE_9); assertLe(rebalancer.budget(), budget); - rebalancer.adjustYieldExposure(amount - (amount * split) / BASE_9, 0, USDC, STEAK_USDC, 0); + rebalancer.adjustYieldExposure(amount - (amount * split) / BASE_9, 0, USDC, STEAK_USDC, 0, new bytes(0)); (fromUSDCPost, totalPost) = transmuter.getIssuedByCollateral(address(USDC)); (fromSTEAKPost, ) = transmuter.getIssuedByCollateral(address(STEAK_USDC)); @@ -247,7 +247,7 @@ contract RebalancerUSDATest is Test { (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); + rebalancer.adjustYieldExposure((amount * split) / BASE_9, 1, USDC, STEAK_USDC, 0, new bytes(0)); (uint256 fromUSDCPost, uint256 totalPost) = transmuter.getIssuedByCollateral(address(USDC)); (uint256 fromSTEAKPost, ) = transmuter.getIssuedByCollateral(address(STEAK_USDC)); @@ -257,7 +257,7 @@ contract RebalancerUSDATest is Test { assertGe(fromSTEAKPost, fromSTEAK); assertLe(rebalancer.budget(), budget); - rebalancer.adjustYieldExposure(amount - (amount * split) / BASE_9, 1, USDC, STEAK_USDC, 0); + rebalancer.adjustYieldExposure(amount - (amount * split) / BASE_9, 1, USDC, STEAK_USDC, 0, new bytes(0)); (fromUSDCPost, totalPost) = transmuter.getIssuedByCollateral(address(USDC)); (fromSTEAKPost, ) = transmuter.getIssuedByCollateral(address(STEAK_USDC)); @@ -286,7 +286,7 @@ contract RebalancerUSDATest is Test { (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); + rebalancer.adjustYieldExposure(amount, 1, USDC, STEAK_USDC, 0, new bytes(0)); (uint256 fromUSDCPost, uint256 totalPost) = transmuter.getIssuedByCollateral(address(USDC)); (uint256 fromSTEAKPost, ) = transmuter.getIssuedByCollateral(address(STEAK_USDC)); @@ -300,7 +300,7 @@ contract RebalancerUSDATest is Test { assertEq(newOrder0, orderBudget0); assertLe(newOrder1, orderBudget1); - rebalancer.adjustYieldExposure(amount, 0, USDC, STEAK_USDC, 0); + rebalancer.adjustYieldExposure(amount, 0, USDC, STEAK_USDC, 0, new bytes(0)); (orderBudget0, , , ) = rebalancer.orders(address(USDC), address(STEAK_USDC)); (orderBudget1, , , ) = rebalancer.orders(address(STEAK_USDC), address(USDC)); diff --git a/utils/forwardUtils.js b/utils/forwardUtils.js index 67f9ddc6..134df1cd 100755 --- a/utils/forwardUtils.js +++ b/utils/forwardUtils.js @@ -11,11 +11,11 @@ const extraArgs = process.argv.slice(3).join(' '); exec(`node lib/utils/utils/${command}.js ${extraArgs}`, (error, stdout, stderr) => { if (error) { console.log(error); - return; + process.exit(1); } if (stderr) { console.log(stderr); - return; + process.exit(1); } console.log(stdout); });