From 495650938bab668808cc493a53ad7bb54dffe800 Mon Sep 17 00:00:00 2001 From: 0xtekgrinder <0xtekgrinder@protonmail.com> Date: Fri, 13 Sep 2024 11:39:05 +0200 Subject: [PATCH 01/69] feat: first version of generic harvester --- contracts/harvester/GenericHarvester.sol | 336 +++++++++++++++++++++++ contracts/utils/Errors.sol | 1 + 2 files changed, 337 insertions(+) create mode 100644 contracts/harvester/GenericHarvester.sol diff --git a/contracts/harvester/GenericHarvester.sol b/contracts/harvester/GenericHarvester.sol new file mode 100644 index 00000000..b0101f68 --- /dev/null +++ b/contracts/harvester/GenericHarvester.sol @@ -0,0 +1,336 @@ +// SPDX-License-Identifier: GPL-3.0 + +pragma solidity ^0.8.23; + +import { SafeCast } from "oz/utils/math/SafeCast.sol"; +import { SafeERC20 } from "oz/token/ERC20/utils/SafeERC20.sol"; +import { IERC20 } from "oz/interfaces/IERC20.sol"; + +import { ITransmuter } from "interfaces/ITransmuter.sol"; + +import { AccessControl, IAccessControlManager } from "../utils/AccessControl.sol"; +import "../utils/Constants.sol"; +import "../utils/Errors.sol"; + +import { IERC3156FlashBorrower } from "oz/interfaces/IERC3156FlashBorrower.sol"; +import { IERC3156FlashLender } from "oz/interfaces/IERC3156FlashLender.sol"; + +struct CollatParams { + // 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 asset + uint64 maxExposureYieldAsset; + // 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 GenericHarvester +/// @author Angle Labs, Inc. +/// @dev Generic contract for anyone to permissionlessly adjust the reserves of Angle Transmuter +contract GenericHarvester is AccessControl, IERC3156FlashBorrower { + using SafeCast for uint256; + using SafeERC20 for IERC20; + bytes32 public constant CALLBACK_SUCCESS = keccak256("ERC3156FlashBorrower.onFlashLoan"); + + /// @notice Angle stablecoin flashloan contract + IERC3156FlashLender public immutable FLASHLOAN; + + /// @notice Reference to the `transmuter` implementation this contract aims at rebalancing + ITransmuter public immutable TRANSMUTER; + /// @notice AgToken handled by the `transmuter` of interest + address public immutable AGTOKEN; + /// @notice Max slippage when dealing with the Transmuter + uint96 public maxSlippage; + /// @notice Data associated to a collateral + mapping(address => CollatParams) public collateralData; + /// @notice Budget of AGToken available for each users + mapping(address => uint256) public budget; + + /*////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + INITIALIZATION + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////*/ + + constructor( + address transmuter, + address collateral, + address asset, + address flashloan, + uint64 targetExposure, + uint64 overrideExposures, + uint64 maxExposureYieldAsset, + uint64 minExposureYieldAsset, + uint96 _maxSlippage + ) { + if (flashloan == address(0)) revert ZeroAddress(); + FLASHLOAN = IERC3156FlashLender(flashloan); + TRANSMUTER = ITransmuter(transmuter); + AGTOKEN = address(ITransmuter(transmuter).agToken()); + + IERC20(AGTOKEN).safeApprove(flashloan, type(uint256).max); + accessControlManager = IAccessControlManager(ITransmuter(transmuter).accessControlManager()); + _setCollateralData( + collateral, + asset, + targetExposure, + minExposureYieldAsset, + maxExposureYieldAsset, + overrideExposures + ); + _setMaxSlippage(_maxSlippage); + } + + /*////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + REBALANCE + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////*/ + + /// @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, 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 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(msg.sender, increase, collateral, asset, minAmountOut, extraData) + ); + } + + /// @inheritdoc IERC3156FlashBorrower + function onFlashLoan( + address initiator, + address, + uint256 amount, + uint256 fee, + bytes calldata data + ) public virtual returns (bytes32) { + if (msg.sender != address(FLASHLOAN) || initiator != address(this) || fee != 0) revert NotTrusted(); + ( + address sender, + uint256 typeAction, + address collateral, + address asset, + uint256 minAmountOut, + bytes memory callData + ) = abi.decode(data, (address, uint256, address, address, uint256, bytes)); + address tokenOut; + address tokenIn; + if (typeAction == 1) { + // Increase yield exposure action: we bring in the yield bearing asset + tokenOut = collateral; + tokenIn = asset; + } else { + // Decrease yield exposure action: we bring in the liquid asset + tokenIn = collateral; + tokenOut = asset; + } + uint256 amountOut = TRANSMUTER.swapExactInput(amount, 0, AGTOKEN, tokenOut, address(this), block.timestamp); + + // Swap to tokenIn + amountOut = _swapToTokenIn(typeAction, tokenIn, tokenOut, amountOut, callData); + + _adjustAllowance(tokenIn, address(TRANSMUTER), amountOut); + uint256 amountStableOut = TRANSMUTER.swapExactInput( + amountOut, + minAmountOut, + tokenIn, + AGTOKEN, + address(this), + block.timestamp + ); + if (amount > amountStableOut) { + // TODO temporary fix for for subsidy as stack too deep + if (budget[sender] < amount - amountStableOut) revert InsufficientFunds(); + budget[sender] -= amount - amountStableOut; + } + return CALLBACK_SUCCESS; + } + + /** + * @notice Add budget to a receiver + * @param amount amount of AGToken to add to the budget + * @param receiver address of the receiver + */ + function addBudget(uint256 amount, address receiver) public virtual { + IERC20(AGTOKEN).safeTransferFrom(msg.sender, address(this), amount); + + budget[receiver] += amount; + } + + /** + * @notice Remove budget from a receiver + * @param amount amount of AGToken to remove from the budget + * @param receiver address of the receiver + */ + function removeBudget(uint256 amount, address receiver) public virtual { + if (budget[receiver] < amount) revert InsufficientFunds(); + budget[receiver] -= amount; + + IERC20(AGTOKEN).safeTransfer(receiver, amount); + } + + /** + * @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) {} + + /*////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + HARVEST + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////*/ + + /// @notice Invests or divests from the yield asset associated to `collateral` based on the current exposure to this + /// collateral + /// @dev This transaction either reduces the exposure to `collateral` in the Transmuter or frees up some collateral + /// 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 + 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 stablecoinsFromAsset, ) = TRANSMUTER.getIssuedByCollateral(collatInfo.asset); + uint8 increase; + uint256 amount; + uint256 targetExposureScaled = collatInfo.targetExposure * stablecoinsIssued; + if (stablecoinsFromCollateral * 1e9 > targetExposureScaled) { + // Need to increase exposure to yield bearing asset + increase = 1; + amount = stablecoinsFromCollateral - targetExposureScaled / 1e9; + uint256 maxValueScaled = collatInfo.maxExposureYieldAsset * stablecoinsIssued; + // These checks assume that there are no transaction fees on the stablecoin->collateral conversion and so + // it's still possible that exposure goes above the max exposure in some rare cases + 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 (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.asset) {} catch {} + + adjustYieldExposure( + amount, + increase, + collateral, + collatInfo.asset, + (amount * (1e9 - maxSlippage)) / 1e9, + extraData + ); + } + } + + /*////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + SETTERS + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////*/ + + function setCollateralData( + address collateral, + address asset, + uint64 targetExposure, + uint64 minExposureYieldAsset, + uint64 maxExposureYieldAsset, + uint64 overrideExposures + ) public virtual onlyGuardian { + _setCollateralData( + collateral, + asset, + targetExposure, + minExposureYieldAsset, + maxExposureYieldAsset, + overrideExposures + ); + } + + function setMaxSlippage(uint96 _maxSlippage) public virtual onlyGuardian { + _setMaxSlippage(_maxSlippage); + } + + function updateLimitExposuresYieldAsset(address collateral) public virtual { + CollatParams storage collatInfo = collateralData[collateral]; + if (collatInfo.overrideExposures == 2) _updateLimitExposuresYieldAsset(collatInfo); + } + + function _setMaxSlippage(uint96 _maxSlippage) internal virtual { + if (_maxSlippage > 1e9) revert InvalidParam(); + maxSlippage = _maxSlippage; + } + + function _setCollateralData( + address collateral, + address asset, + uint64 targetExposure, + uint64 minExposureYieldAsset, + uint64 maxExposureYieldAsset, + uint64 overrideExposures + ) internal virtual { + CollatParams storage collatInfo = collateralData[collateral]; + collatInfo.asset = asset; + if (targetExposure >= 1e9) revert InvalidParam(); + collatInfo.targetExposure = targetExposure; + collatInfo.overrideExposures = overrideExposures; + if (overrideExposures == 1) { + if (maxExposureYieldAsset >= 1e9 || minExposureYieldAsset >= maxExposureYieldAsset) revert InvalidParam(); + collatInfo.maxExposureYieldAsset = maxExposureYieldAsset; + collatInfo.minExposureYieldAsset = minExposureYieldAsset; + } else { + collatInfo.overrideExposures = 2; + _updateLimitExposuresYieldAsset(collatInfo); + } + } + + function _updateLimitExposuresYieldAsset(CollatParams storage collatInfo) internal virtual { + uint64[] memory xFeeMint; + (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.asset); + length = xFeeBurn.length; + if (length <= 1) collatInfo.minExposureYieldAsset = 0; + else collatInfo.minExposureYieldAsset = xFeeBurn[length - 2]; + } + + /*////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + 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/utils/Errors.sol b/contracts/utils/Errors.sol index 855c8099..7f553756 100644 --- a/contracts/utils/Errors.sol +++ b/contracts/utils/Errors.sol @@ -44,3 +44,4 @@ error ZeroAddress(); error ZeroAmount(); error SwapError(); error SlippageTooHigh(); +error InsufficientFunds(); From 513009b30d01f407120003a8c5e9ef466858d23d Mon Sep 17 00:00:00 2001 From: 0xtekgrinder <0xtekgrinder@protonmail.com> Date: Fri, 13 Sep 2024 12:54:31 +0200 Subject: [PATCH 02/69] feat: generic harvester swap and vault --- contracts/harvester/GenericHarvesterSwap.sol | 110 ++++++++++++++++++ contracts/harvester/GenericHarvesterVault.sol | 59 ++++++++++ 2 files changed, 169 insertions(+) create mode 100644 contracts/harvester/GenericHarvesterSwap.sol create mode 100644 contracts/harvester/GenericHarvesterVault.sol diff --git a/contracts/harvester/GenericHarvesterSwap.sol b/contracts/harvester/GenericHarvesterSwap.sol new file mode 100644 index 00000000..1d6f209a --- /dev/null +++ b/contracts/harvester/GenericHarvesterSwap.sol @@ -0,0 +1,110 @@ +// SPDX-License-Identifier: GPL-3.0 + +pragma solidity ^0.8.23; + +import "./GenericHarvester.sol"; +import { IERC20Metadata } from "oz/interfaces/IERC20Metadata.sol"; +import { RouterSwapper } from "utils/src/RouterSwapper.sol"; + +/// @title GenericHarvesterSwap +/// @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 GenericHarvesterSwap is GenericHarvester, RouterSwapper { + using SafeCast for uint256; + + uint32 public maxSwapSlippage; + + constructor( + address transmuter, + address collateral, + address asset, + address flashloan, + uint64 targetExposure, + uint64 overrideExposures, + uint64 maxExposureYieldAsset, + uint64 minExposureYieldAsset, + uint32 _maxSlippage, + address _tokenTransferAddress, + address _swapRouter, + uint32 _maxSwapSlippage + ) + GenericHarvester( + transmuter, + collateral, + asset, + flashloan, + targetExposure, + overrideExposures, + maxExposureYieldAsset, + minExposureYieldAsset, + _maxSlippage + ) + RouterSwapper(_swapRouter, _tokenTransferAddress) + { + maxSwapSlippage = _maxSwapSlippage; + } + + /** + * @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 swap slippage + * @param _maxSwapSlippage max slippage in BPS + */ + function setMaxSwapSlippage(uint32 _maxSwapSlippage) external onlyGuardian { + maxSwapSlippage = _maxSwapSlippage; + } + + /** + * @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 - maxSwapSlippage)) / BPS) { + revert SlippageTooHigh(); + } + return amountOut; + } +} diff --git a/contracts/harvester/GenericHarvesterVault.sol b/contracts/harvester/GenericHarvesterVault.sol new file mode 100644 index 00000000..2ac7025c --- /dev/null +++ b/contracts/harvester/GenericHarvesterVault.sol @@ -0,0 +1,59 @@ +// SPDX-License-Identifier: GPL-3.0 + +pragma solidity ^0.8.19; + +import "./GenericHarvester.sol"; +import { IERC4626 } from "interfaces/external/IERC4626.sol"; + +/// @title GenericHarvesterVault +/// @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 GenericHarvesterVault is GenericHarvester { + using SafeCast for uint256; + + constructor( + address transmuter, + address collateral, + address asset, + address flashloan, + uint64 targetExposure, + uint64 overrideExposures, + uint64 maxExposureYieldAsset, + uint64 minExposureYieldAsset, + uint32 _maxSlippage + ) + GenericHarvester( + transmuter, + collateral, + asset, + flashloan, + targetExposure, + overrideExposures, + maxExposureYieldAsset, + minExposureYieldAsset, + _maxSlippage + ) + {} + + /** + * @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)); + } +} From 455ea990bfe9d788c7509664d593baf78afc0cea Mon Sep 17 00:00:00 2001 From: 0xtekgrinder <0xtekgrinder@protonmail.com> Date: Fri, 13 Sep 2024 16:14:04 +0200 Subject: [PATCH 03/69] feat: remove access control from adjustYield --- contracts/harvester/GenericHarvester.sol | 1 - 1 file changed, 1 deletion(-) diff --git a/contracts/harvester/GenericHarvester.sol b/contracts/harvester/GenericHarvester.sol index b0101f68..4224fe95 100644 --- a/contracts/harvester/GenericHarvester.sol +++ b/contracts/harvester/GenericHarvester.sol @@ -102,7 +102,6 @@ contract GenericHarvester is AccessControl, IERC3156FlashBorrower { uint256 minAmountOut, bytes calldata extraData ) public virtual { - if (!TRANSMUTER.isTrustedSeller(msg.sender)) revert NotTrusted(); FLASHLOAN.flashLoan( IERC3156FlashBorrower(address(this)), address(AGTOKEN), From 8b0a95dce225e4cf1349cfff69bbbb293e3bd566 Mon Sep 17 00:00:00 2001 From: 0xtekgrinder <0xtekgrinder@protonmail.com> Date: Mon, 16 Sep 2024 15:09:43 +0200 Subject: [PATCH 04/69] feat: merge genericHarvester into a single contract --- contracts/harvester/GenericHarvesterSwap.sol | 110 ------------ contracts/harvester/GenericHarvesterVault.sol | 59 ------- .../GenericHarvester.sol | 160 ++++++++++++++---- 3 files changed, 131 insertions(+), 198 deletions(-) delete mode 100644 contracts/harvester/GenericHarvesterSwap.sol delete mode 100644 contracts/harvester/GenericHarvesterVault.sol rename contracts/{harvester => helpers}/GenericHarvester.sol (74%) diff --git a/contracts/harvester/GenericHarvesterSwap.sol b/contracts/harvester/GenericHarvesterSwap.sol deleted file mode 100644 index 1d6f209a..00000000 --- a/contracts/harvester/GenericHarvesterSwap.sol +++ /dev/null @@ -1,110 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0 - -pragma solidity ^0.8.23; - -import "./GenericHarvester.sol"; -import { IERC20Metadata } from "oz/interfaces/IERC20Metadata.sol"; -import { RouterSwapper } from "utils/src/RouterSwapper.sol"; - -/// @title GenericHarvesterSwap -/// @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 GenericHarvesterSwap is GenericHarvester, RouterSwapper { - using SafeCast for uint256; - - uint32 public maxSwapSlippage; - - constructor( - address transmuter, - address collateral, - address asset, - address flashloan, - uint64 targetExposure, - uint64 overrideExposures, - uint64 maxExposureYieldAsset, - uint64 minExposureYieldAsset, - uint32 _maxSlippage, - address _tokenTransferAddress, - address _swapRouter, - uint32 _maxSwapSlippage - ) - GenericHarvester( - transmuter, - collateral, - asset, - flashloan, - targetExposure, - overrideExposures, - maxExposureYieldAsset, - minExposureYieldAsset, - _maxSlippage - ) - RouterSwapper(_swapRouter, _tokenTransferAddress) - { - maxSwapSlippage = _maxSwapSlippage; - } - - /** - * @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 swap slippage - * @param _maxSwapSlippage max slippage in BPS - */ - function setMaxSwapSlippage(uint32 _maxSwapSlippage) external onlyGuardian { - maxSwapSlippage = _maxSwapSlippage; - } - - /** - * @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 - maxSwapSlippage)) / BPS) { - revert SlippageTooHigh(); - } - return amountOut; - } -} diff --git a/contracts/harvester/GenericHarvesterVault.sol b/contracts/harvester/GenericHarvesterVault.sol deleted file mode 100644 index 2ac7025c..00000000 --- a/contracts/harvester/GenericHarvesterVault.sol +++ /dev/null @@ -1,59 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0 - -pragma solidity ^0.8.19; - -import "./GenericHarvester.sol"; -import { IERC4626 } from "interfaces/external/IERC4626.sol"; - -/// @title GenericHarvesterVault -/// @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 GenericHarvesterVault is GenericHarvester { - using SafeCast for uint256; - - constructor( - address transmuter, - address collateral, - address asset, - address flashloan, - uint64 targetExposure, - uint64 overrideExposures, - uint64 maxExposureYieldAsset, - uint64 minExposureYieldAsset, - uint32 _maxSlippage - ) - GenericHarvester( - transmuter, - collateral, - asset, - flashloan, - targetExposure, - overrideExposures, - maxExposureYieldAsset, - minExposureYieldAsset, - _maxSlippage - ) - {} - - /** - * @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/harvester/GenericHarvester.sol b/contracts/helpers/GenericHarvester.sol similarity index 74% rename from contracts/harvester/GenericHarvester.sol rename to contracts/helpers/GenericHarvester.sol index 4224fe95..bb8c144b 100644 --- a/contracts/harvester/GenericHarvester.sol +++ b/contracts/helpers/GenericHarvester.sol @@ -5,8 +5,10 @@ pragma solidity ^0.8.23; import { SafeCast } from "oz/utils/math/SafeCast.sol"; import { SafeERC20 } from "oz/token/ERC20/utils/SafeERC20.sol"; import { IERC20 } from "oz/interfaces/IERC20.sol"; +import { IERC20Metadata } from "oz/interfaces/IERC20Metadata.sol"; import { ITransmuter } from "interfaces/ITransmuter.sol"; +import { RouterSwapper } from "utils/src/RouterSwapper.sol"; import { AccessControl, IAccessControlManager } from "../utils/AccessControl.sol"; import "../utils/Constants.sol"; @@ -14,6 +16,7 @@ import "../utils/Errors.sol"; import { IERC3156FlashBorrower } from "oz/interfaces/IERC3156FlashBorrower.sol"; import { IERC3156FlashLender } from "oz/interfaces/IERC3156FlashLender.sol"; +import { IERC4626 } from "interfaces/external/IERC4626.sol"; struct CollatParams { // Yield bearing asset associated to the collateral @@ -29,14 +32,21 @@ struct CollatParams { uint64 overrideExposures; } +enum SwapType { + VAULT, + SWAP +} + /// @title GenericHarvester /// @author Angle Labs, Inc. /// @dev Generic contract for anyone to permissionlessly adjust the reserves of Angle Transmuter -contract GenericHarvester is AccessControl, IERC3156FlashBorrower { +contract GenericHarvester is AccessControl, IERC3156FlashBorrower, RouterSwapper { using SafeCast for uint256; using SafeERC20 for IERC20; bytes32 public constant CALLBACK_SUCCESS = keccak256("ERC3156FlashBorrower.onFlashLoan"); + uint32 public maxSwapSlippage; + /// @notice Angle stablecoin flashloan contract IERC3156FlashLender public immutable FLASHLOAN; @@ -64,8 +74,11 @@ contract GenericHarvester is AccessControl, IERC3156FlashBorrower { uint64 overrideExposures, uint64 maxExposureYieldAsset, uint64 minExposureYieldAsset, - uint96 _maxSlippage - ) { + uint96 _maxSlippage, + address _tokenTransferAddress, + address _swapRouter, + uint32 _maxSwapSlippage + ) RouterSwapper(_swapRouter, _tokenTransferAddress) { if (flashloan == address(0)) revert ZeroAddress(); FLASHLOAN = IERC3156FlashLender(flashloan); TRANSMUTER = ITransmuter(transmuter); @@ -81,6 +94,7 @@ contract GenericHarvester is AccessControl, IERC3156FlashBorrower { maxExposureYieldAsset, overrideExposures ); + maxSwapSlippage = _maxSwapSlippage; _setMaxSlippage(_maxSlippage); } @@ -100,13 +114,14 @@ contract GenericHarvester is AccessControl, IERC3156FlashBorrower { address collateral, address asset, uint256 minAmountOut, + SwapType swapType, bytes calldata extraData ) public virtual { FLASHLOAN.flashLoan( IERC3156FlashBorrower(address(this)), address(AGTOKEN), amountStablecoins, - abi.encode(msg.sender, increase, collateral, asset, minAmountOut, extraData) + abi.encode(msg.sender, increase, collateral, asset, minAmountOut, swapType, extraData) ); } @@ -119,29 +134,34 @@ contract GenericHarvester is AccessControl, IERC3156FlashBorrower { bytes calldata data ) public virtual returns (bytes32) { if (msg.sender != address(FLASHLOAN) || initiator != address(this) || fee != 0) revert NotTrusted(); - ( - address sender, - uint256 typeAction, - address collateral, - address asset, - uint256 minAmountOut, - bytes memory callData - ) = abi.decode(data, (address, uint256, address, address, uint256, bytes)); + address sender; + uint256 typeAction; + uint256 minAmountOut; + SwapType swapType; + bytes memory callData; address tokenOut; address tokenIn; - if (typeAction == 1) { - // Increase yield exposure action: we bring in the yield bearing asset - tokenOut = collateral; - tokenIn = asset; - } else { - // Decrease yield exposure action: we bring in the liquid asset - tokenIn = collateral; - tokenOut = asset; + { + address collateral; + address asset; + (sender, typeAction, collateral, asset, minAmountOut, swapType, callData) = abi.decode( + data, + (address, uint256, address, address, uint256, SwapType, bytes) + ); + if (typeAction == 1) { + // Increase yield exposure action: we bring in the yield bearing asset + tokenOut = collateral; + tokenIn = asset; + } else { + // Decrease yield exposure action: we bring in the liquid asset + tokenIn = collateral; + tokenOut = asset; + } } uint256 amountOut = TRANSMUTER.swapExactInput(amount, 0, AGTOKEN, tokenOut, address(this), block.timestamp); // Swap to tokenIn - amountOut = _swapToTokenIn(typeAction, tokenIn, tokenOut, amountOut, callData); + amountOut = _swapToTokenIn(typeAction, tokenIn, tokenOut, amountOut, swapType, callData); _adjustAllowance(tokenIn, address(TRANSMUTER), amountOut); uint256 amountStableOut = TRANSMUTER.swapExactInput( @@ -183,21 +203,78 @@ contract GenericHarvester is AccessControl, IERC3156FlashBorrower { IERC20(AGTOKEN).safeTransfer(receiver, amount); } + function _swapToTokenIn( + uint256 typeAction, + address tokenIn, + address tokenOut, + uint256 amount, + SwapType swapType, + bytes memory callData + ) internal returns (uint256) { + if (swapType == SwapType.SWAP) { + return _swapToTokenInSwap(tokenIn, tokenOut, amount, callData); + } else if (swapType == SwapType.VAULT) { + return _swapToTokenInVault(typeAction, tokenIn, tokenOut, amount); + } + } + /** - * @dev hook to swap from tokenOut to tokenIn - * @param typeAction 1 for deposit, 2 for redeem + * @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 extra call data (if needed) + * @param callData bytes to call the router/aggregator */ - function _swapToTokenIn( - uint256 typeAction, + function _swapToTokenInSwap( address tokenIn, address tokenOut, uint256 amount, bytes memory callData - ) internal virtual returns (uint256) {} + ) internal 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 - maxSwapSlippage)) / BPS) { + revert SlippageTooHigh(); + } + return amountOut; + } + + /** + * @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 _swapToTokenInVault( + uint256 typeAction, + address tokenIn, + address tokenOut, + uint256 amount + ) internal 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)); + } /*////////////////////////////////////////////////////////////////////////////////////////////////////////////////// HARVEST @@ -209,7 +286,7 @@ contract GenericHarvester is AccessControl, IERC3156FlashBorrower { /// 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 - function harvest(address collateral, uint256 scale, bytes calldata extraData) public virtual { + function harvest(address collateral, uint256 scale, SwapType swapType, bytes calldata extraData) public virtual { if (scale > 1e9) revert InvalidParam(); (uint256 stablecoinsFromCollateral, uint256 stablecoinsIssued) = TRANSMUTER.getIssuedByCollateral(collateral); CollatParams memory collatInfo = collateralData[collateral]; @@ -246,6 +323,7 @@ contract GenericHarvester is AccessControl, IERC3156FlashBorrower { collateral, collatInfo.asset, (amount * (1e9 - maxSlippage)) / 1e9, + swapType, extraData ); } @@ -277,7 +355,31 @@ contract GenericHarvester is AccessControl, IERC3156FlashBorrower { _setMaxSlippage(_maxSlippage); } - function updateLimitExposuresYieldAsset(address collateral) public virtual { + /** + * @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 swap slippage + * @param _maxSwapSlippage max slippage in BPS + */ + function setMaxSwapSlippage(uint32 _maxSwapSlippage) external onlyGuardian { + maxSwapSlippage = _maxSwapSlippage; + } + + function updateLimitExposuresYieldAsset(address collateral) public virtual onlyGuardian { CollatParams storage collatInfo = collateralData[collateral]; if (collatInfo.overrideExposures == 2) _updateLimitExposuresYieldAsset(collatInfo); } From f6205028c9ce95d126f1082ab7986d71609cdb8a Mon Sep 17 00:00:00 2001 From: 0xtekgrinder <0xtekgrinder@protonmail.com> Date: Tue, 17 Sep 2024 16:21:25 +0200 Subject: [PATCH 05/69] feat: multi block rebalancer --- contracts/helpers/MultiBlockRebalancer.sol | 227 +++++++++++++++++++++ contracts/interfaces/IPool.sol | 9 + contracts/utils/Constants.sol | 4 + 3 files changed, 240 insertions(+) create mode 100644 contracts/helpers/MultiBlockRebalancer.sol create mode 100644 contracts/interfaces/IPool.sol diff --git a/contracts/helpers/MultiBlockRebalancer.sol b/contracts/helpers/MultiBlockRebalancer.sol new file mode 100644 index 00000000..066f074e --- /dev/null +++ b/contracts/helpers/MultiBlockRebalancer.sol @@ -0,0 +1,227 @@ +// SPDX-License-Identifier: GPL-3.0 + +pragma solidity ^0.8.23; + +import { AccessControl, IAccessControlManager } from "../utils/AccessControl.sol"; +import { IAgToken } from "../interfaces/IAgToken.sol"; +import { IPool } from "../interfaces/IPool.sol"; +import { ITransmuter } from "../interfaces/ITransmuter.sol"; +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; + +import "../utils/Errors.sol"; +import "../utils/Constants.sol"; + +struct CollatParams { + // 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 asset + uint64 maxExposureYieldAsset; + // 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; +} + +contract MultiBlockRebalancer is AccessControl { + using SafeERC20 for IERC20; + + mapping(address => address) public collateralToDepositAddress; + /// @notice Data associated to a collateral + mapping(address => CollatParams) public collateralData; + + uint256 public maxMintAmount; + ITransmuter public transmuter; + IAgToken public agToken; + + constructor( + uint256 initialMaxMintAmount, + IAccessControlManager definitiveAccessControlManager, + IAgToken definitiveAgToken, + ITransmuter definitivetransmuter + ) { + accessControlManager = definitiveAccessControlManager; + agToken = definitiveAgToken; + transmuter = definitivetransmuter; + } + + /*////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + GOVERNOR FUNCTIONS + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////*/ + + function setMaxMintAmount(uint256 newMaxMintAmount) external onlyGovernor { + maxMintAmount = newMaxMintAmount; + } + + /*////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + GUARDIAN FUNCTIONS + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////*/ + + function SetCollateralData( + address collateral, + address asset, + uint64 targetExposure, + uint64 minExposureYieldAsset, + uint64 maxExposureYieldAsset, + uint64 overrideExposures + ) external onlyGuardian { + CollatParams storage collatInfo = collateralData[collateral]; + collatInfo.asset = asset; + if (targetExposure >= 1e9) revert InvalidParam(); + collatInfo.targetExposure = targetExposure; + collatInfo.overrideExposures = overrideExposures; + if (overrideExposures == 1) { + if (maxExposureYieldAsset >= 1e9 || minExposureYieldAsset >= maxExposureYieldAsset) revert InvalidParam(); + collatInfo.maxExposureYieldAsset = maxExposureYieldAsset; + collatInfo.minExposureYieldAsset = minExposureYieldAsset; + } else { + collatInfo.overrideExposures = 2; + _updateLimitExposuresYieldAsset(collatInfo); + } + } + + function setCollateralToDepositAddress(address collateral, address newDepositAddress) external onlyGuardian { + collateralToDepositAddress[collateral] = newDepositAddress; + } + + function initiateRebalance(uint256 scale, address collateral) external onlyGuardian { + if (scale > 1e9) revert InvalidParam(); + (uint8 increase, uint256 amount) = _computeRebalanceAmount(collateral); + amount = (amount * scale) / 1e9; + + _rebalance(increase, collateral, amount); + } + + function finalizeRebalance(address collateral) external onlyGuardian { + // TODO handle increase rebalance for USDM + uint256 balance = IERC20(collateral).balanceOf(address(this)); + + if (collateral == USDM) { + _adjustAllowance(address(agToken), address(transmuter), balance); + uint256 amountOut = transmuter.swapExactInput( + balance, + 0, + collateral, + address(agToken), + address(this), + block.timestamp + ); + agToken.burnSelf(amountOut, address(this)); + } + } + + /*////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + INTERNAL FUNCTIONS + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////*/ + + function _computeRebalanceAmount(address collateral) internal view returns (uint8 increase, uint256 amount) { + (uint256 stablecoinsFromCollateral, uint256 stablecoinsIssued) = transmuter.getIssuedByCollateral(collateral); + CollatParams memory collatInfo = collateralData[collateral]; + (uint256 stablecoinsFromAsset, ) = transmuter.getIssuedByCollateral(collatInfo.asset); + uint256 targetExposureScaled = collatInfo.targetExposure * stablecoinsIssued; + if (stablecoinsFromCollateral * 1e9 > targetExposureScaled) { + // Need to increase exposure to yield bearing asset + increase = 1; + amount = stablecoinsFromCollateral - targetExposureScaled / 1e9; + uint256 maxValueScaled = collatInfo.maxExposureYieldAsset * stablecoinsIssued; + // These checks assume that there are no transaction fees on the stablecoin->collateral conversion and so + // it's still possible that exposure goes above the max exposure in some rare cases + 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 (stablecoinsFromAsset * 1e9 < minValueScaled) amount = 0; + else if (stablecoinsFromAsset * 1e9 < minValueScaled + amount * 1e9) + amount = stablecoinsFromAsset - minValueScaled / 1e9; + } + } + + function _rebalance(uint8 typeAction, address collateral, uint256 amount) internal { + if (amount > maxMintAmount) revert TooBigAmountIn(); + agToken.mint(address(this), amount); + if (typeAction == 1) { + _adjustAllowance(address(agToken), address(transmuter), amount); + address depositAddresss = collateralToDepositAddress[collateral]; + + if (collateral == XEVT) { + uint256 amountOut = transmuter.swapExactInput( + amount, + 0, + address(agToken), + EURC, + address(this), + block.timestamp + ); + _adjustAllowance(collateral, address(depositAddresss), amountOut); + (uint256 shares, ) = IPool(depositAddresss).deposit(amountOut, address(this)); // TODO update lender address + amountOut = transmuter.swapExactInput( + shares, + 0, + collateral, + address(agToken), + address(this), + block.timestamp + ); + agToken.burnSelf(amountOut, address(this)); + } else if (collateral == USDM) { + uint256 amountOut = transmuter.swapExactInput( + amount, + 0, + address(agToken), + USDC, + address(this), + block.timestamp + ); + _adjustAllowance(collateral, address(depositAddresss), amountOut); + IERC20(collateral).transfer(depositAddresss, amountOut); + } + } else { + _adjustAllowance(address(agToken), address(transmuter), amount); + uint256 amountOut = transmuter.swapExactInput( + amount, + 0, + address(agToken), + collateral, + address(this), + block.timestamp + ); + address depositAddresss = collateralToDepositAddress[collateral]; + + if (collateral == XEVT) { + IPool(depositAddresss).requestRedeem(amountOut); + } else if (collateral == USDM) { + IERC20(collateral).transfer(depositAddresss, amountOut); + } + } + } + + function _updateLimitExposuresYieldAsset(CollatParams storage collatInfo) internal virtual { + uint64[] memory xFeeMint; + (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.asset); + length = xFeeBurn.length; + if (length <= 1) collatInfo.minExposureYieldAsset = 0; + else collatInfo.minExposureYieldAsset = xFeeBurn[length - 2]; + } + + /*////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + 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/interfaces/IPool.sol b/contracts/interfaces/IPool.sol new file mode 100644 index 00000000..80cf608b --- /dev/null +++ b/contracts/interfaces/IPool.sol @@ -0,0 +1,9 @@ +// SPDX-License-Identifier: GPL-3.0 + +pragma solidity >=0.5.0; + +interface IPool { + function deposit(uint256 assets, address lender) external returns (uint256 shares, uint256 transferInDayTimestamp); + + function requestRedeem(uint256 shares) external returns (uint256 assets); +} diff --git a/contracts/utils/Constants.sol b/contracts/utils/Constants.sol index 34fcdd83..f992da83 100644 --- a/contracts/utils/Constants.sol +++ b/contracts/utils/Constants.sol @@ -62,3 +62,7 @@ ICbETH constant CBETH = ICbETH(0xBe9895146f7AF43049ca1c1AE358B0541Ea49704); IRETH constant RETH = IRETH(0xae78736Cd615f374D3085123A210448E74Fc6393); IStETH constant STETH = IStETH(0xae7ab96520DE3A18E5e111B5EaAb095312D7fE84); ISfrxETH constant SFRXETH = ISfrxETH(0xac3E018457B222d93114458476f3E3416Abbe38F); +address constant XEVT = 0x3Ee320c9F73a84D1717557af00695A34b26d1F1d; +address constant USDM = 0x59D9356E565Ab3A36dD77763Fc0d87fEaf85508C; +address constant USDC = 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48; +address constant EURC = 0x1aBaEA1f7C830bD89Acc67eC4af516284b1bC33c; From 875665fa55ecb3b5adbca727d0441ed092b853c5 Mon Sep 17 00:00:00 2001 From: 0xtekgrinder <0xtekgrinder@protonmail.com> Date: Tue, 17 Sep 2024 19:09:53 +0200 Subject: [PATCH 06/69] feat: correct permissions for MultiMockRebalancer --- contracts/helpers/MultiBlockRebalancer.sol | 100 +++++++++++++++------ 1 file changed, 74 insertions(+), 26 deletions(-) diff --git a/contracts/helpers/MultiBlockRebalancer.sol b/contracts/helpers/MultiBlockRebalancer.sol index 066f074e..bf194421 100644 --- a/contracts/helpers/MultiBlockRebalancer.sol +++ b/contracts/helpers/MultiBlockRebalancer.sol @@ -13,8 +13,6 @@ import "../utils/Errors.sol"; import "../utils/Constants.sol"; struct CollatParams { - // 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 asset @@ -29,20 +27,42 @@ struct CollatParams { contract MultiBlockRebalancer is AccessControl { using SafeERC20 for IERC20; + /*////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + MODIFIERS + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////*/ + + modifier onlyTrusted() { + if (!isTrusted[msg.sender]) revert NotTrusted(); + _; + } + + /*////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + VARIABLES + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////*/ + + /// @notice address to deposit to receive collateral mapping(address => address) public collateralToDepositAddress; /// @notice Data associated to a collateral mapping(address => CollatParams) public collateralData; + /// @notice trusted addresses + mapping(address => bool) public isTrusted; + /// @notice Maximum amount of stablecoins that can be minted in a single transaction uint256 public maxMintAmount; ITransmuter public transmuter; IAgToken public agToken; + /*////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + CONSTRUCTOR + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////*/ + constructor( uint256 initialMaxMintAmount, IAccessControlManager definitiveAccessControlManager, IAgToken definitiveAgToken, ITransmuter definitivetransmuter ) { + maxMintAmount = initialMaxMintAmount; accessControlManager = definitiveAccessControlManager; agToken = definitiveAgToken; transmuter = definitivetransmuter; @@ -52,6 +72,10 @@ contract MultiBlockRebalancer is AccessControl { GOVERNOR FUNCTIONS //////////////////////////////////////////////////////////////////////////////////////////////////////////////////*/ + /** + * @notice Set the maximum amount of stablecoins that can be minted in a single transaction + * @param newMaxMintAmount new maximum amount of stablecoins that can be minted in a single transaction + */ function setMaxMintAmount(uint256 newMaxMintAmount) external onlyGovernor { maxMintAmount = newMaxMintAmount; } @@ -60,16 +84,23 @@ contract MultiBlockRebalancer is AccessControl { GUARDIAN FUNCTIONS //////////////////////////////////////////////////////////////////////////////////////////////////////////////////*/ - function SetCollateralData( + /** + * @notice Set the collateral data + * @param collateral address of the collateral + * @param targetExposure target exposure to the collateral asset used + * @param minExposureYieldAsset minimum exposure within the Transmuter to the asset + * @param maxExposureYieldAsset maximum exposure within the Transmuter to the asset + * @param overrideExposures 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 + */ + function setCollateralData( address collateral, - address asset, uint64 targetExposure, uint64 minExposureYieldAsset, uint64 maxExposureYieldAsset, uint64 overrideExposures ) external onlyGuardian { CollatParams storage collatInfo = collateralData[collateral]; - collatInfo.asset = asset; if (targetExposure >= 1e9) revert InvalidParam(); collatInfo.targetExposure = targetExposure; collatInfo.overrideExposures = overrideExposures; @@ -79,38 +110,55 @@ contract MultiBlockRebalancer is AccessControl { collatInfo.minExposureYieldAsset = minExposureYieldAsset; } else { collatInfo.overrideExposures = 2; - _updateLimitExposuresYieldAsset(collatInfo); + _updateLimitExposuresYieldAsset(collateral, collatInfo); } } + /** + * @notice Set the deposit address for a collateral + * @param collateral address of the collateral + * @param newDepositAddress address to deposit to receive collateral + */ function setCollateralToDepositAddress(address collateral, address newDepositAddress) external onlyGuardian { collateralToDepositAddress[collateral] = newDepositAddress; } - function initiateRebalance(uint256 scale, address collateral) external onlyGuardian { + /*////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + TRUSTED FUNCTIONS + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////*/ + + /** + * @notice Initiate a rebalance + * @param scale scale to apply to the rebalance amount + * @param collateral address of the collateral + */ + function initiateRebalance(uint256 scale, address collateral) external onlyTrusted { if (scale > 1e9) revert InvalidParam(); (uint8 increase, uint256 amount) = _computeRebalanceAmount(collateral); amount = (amount * scale) / 1e9; + try transmuter.updateOracle(collateral) {} catch {} _rebalance(increase, collateral, amount); } - function finalizeRebalance(address collateral) external onlyGuardian { - // TODO handle increase rebalance for USDM + /** + * @notice Finalize a rebalance + * @param collateral address of the collateral + */ + function finalizeRebalance(address collateral) external onlyTrusted { uint256 balance = IERC20(collateral).balanceOf(address(this)); - if (collateral == USDM) { - _adjustAllowance(address(agToken), address(transmuter), balance); - uint256 amountOut = transmuter.swapExactInput( - balance, - 0, - collateral, - address(agToken), - address(this), - block.timestamp - ); - agToken.burnSelf(amountOut, address(this)); - } + try transmuter.updateOracle(collateral) {} catch {} + _adjustAllowance(address(agToken), address(transmuter), balance); + uint256 amountOut = transmuter.swapExactInput( + balance, + 0, + collateral, + address(agToken), + address(this), + block.timestamp + ); + agToken.burnSelf(amountOut, address(this)); } /*////////////////////////////////////////////////////////////////////////////////////////////////////////////////// @@ -120,7 +168,7 @@ contract MultiBlockRebalancer is AccessControl { function _computeRebalanceAmount(address collateral) internal view returns (uint8 increase, uint256 amount) { (uint256 stablecoinsFromCollateral, uint256 stablecoinsIssued) = transmuter.getIssuedByCollateral(collateral); CollatParams memory collatInfo = collateralData[collateral]; - (uint256 stablecoinsFromAsset, ) = transmuter.getIssuedByCollateral(collatInfo.asset); + (uint256 stablecoinsFromAsset, ) = transmuter.getIssuedByCollateral(collateral); uint256 targetExposureScaled = collatInfo.targetExposure * stablecoinsIssued; if (stablecoinsFromCollateral * 1e9 > targetExposureScaled) { // Need to increase exposure to yield bearing asset @@ -160,7 +208,7 @@ contract MultiBlockRebalancer is AccessControl { block.timestamp ); _adjustAllowance(collateral, address(depositAddresss), amountOut); - (uint256 shares, ) = IPool(depositAddresss).deposit(amountOut, address(this)); // TODO update lender address + (uint256 shares, ) = IPool(depositAddresss).deposit(amountOut, address(this)); amountOut = transmuter.swapExactInput( shares, 0, @@ -202,15 +250,15 @@ contract MultiBlockRebalancer is AccessControl { } } - function _updateLimitExposuresYieldAsset(CollatParams storage collatInfo) internal virtual { + function _updateLimitExposuresYieldAsset(address collateral, CollatParams storage collatInfo) internal virtual { uint64[] memory xFeeMint; - (xFeeMint, ) = transmuter.getCollateralMintFees(collatInfo.asset); + (xFeeMint, ) = transmuter.getCollateralMintFees(collateral); uint256 length = xFeeMint.length; if (length <= 1) collatInfo.maxExposureYieldAsset = 1e9; else collatInfo.maxExposureYieldAsset = xFeeMint[length - 2]; uint64[] memory xFeeBurn; - (xFeeBurn, ) = transmuter.getCollateralBurnFees(collatInfo.asset); + (xFeeBurn, ) = transmuter.getCollateralBurnFees(collateral); length = xFeeBurn.length; if (length <= 1) collatInfo.minExposureYieldAsset = 0; else collatInfo.minExposureYieldAsset = xFeeBurn[length - 2]; From 857caeeb371a603b468e09724117981defb7ec4f Mon Sep 17 00:00:00 2001 From: 0xtekgrinder <0xtekgrinder@protonmail.com> Date: Tue, 17 Sep 2024 19:35:29 +0200 Subject: [PATCH 07/69] feat: slippage handling for MultiblockRebalancer --- contracts/helpers/MultiBlockRebalancer.sol | 58 ++++++++++++++++++---- contracts/interfaces/IPool.sol | 4 ++ 2 files changed, 52 insertions(+), 10 deletions(-) diff --git a/contracts/helpers/MultiBlockRebalancer.sol b/contracts/helpers/MultiBlockRebalancer.sol index bf194421..7fa883a8 100644 --- a/contracts/helpers/MultiBlockRebalancer.sol +++ b/contracts/helpers/MultiBlockRebalancer.sol @@ -49,6 +49,8 @@ contract MultiBlockRebalancer is AccessControl { /// @notice Maximum amount of stablecoins that can be minted in a single transaction uint256 public maxMintAmount; + /// @notice Max slippage when dealing with the Transmuter + uint96 public maxSlippage; ITransmuter public transmuter; IAgToken public agToken; @@ -58,10 +60,12 @@ contract MultiBlockRebalancer is AccessControl { constructor( uint256 initialMaxMintAmount, + uint96 initialMaxSlippage, IAccessControlManager definitiveAccessControlManager, IAgToken definitiveAgToken, ITransmuter definitivetransmuter ) { + _setMaxSlippage(initialMaxSlippage); maxMintAmount = initialMaxMintAmount; accessControlManager = definitiveAccessControlManager; agToken = definitiveAgToken; @@ -114,6 +118,14 @@ contract MultiBlockRebalancer is AccessControl { } } + /** + * @notice Set the max allowed slippage + * @param newMaxSlippage new max allowed slippage + */ + function setMaxSlippage(uint96 newMaxSlippage) external onlyGuardian { + _setMaxSlippage(newMaxSlippage); + } + /** * @notice Set the deposit address for a collateral * @param collateral address of the collateral @@ -158,6 +170,8 @@ contract MultiBlockRebalancer is AccessControl { address(this), block.timestamp ); + address depositAddress = collateral == XEVT ? collateralToDepositAddress[collateral] : address(0); + _checkSlippage(balance, amountOut, collateral, depositAddress); agToken.burnSelf(amountOut, address(this)); } @@ -194,9 +208,9 @@ contract MultiBlockRebalancer is AccessControl { function _rebalance(uint8 typeAction, address collateral, uint256 amount) internal { if (amount > maxMintAmount) revert TooBigAmountIn(); agToken.mint(address(this), amount); + _adjustAllowance(address(agToken), address(transmuter), amount); if (typeAction == 1) { - _adjustAllowance(address(agToken), address(transmuter), amount); - address depositAddresss = collateralToDepositAddress[collateral]; + address depositAddress = collateralToDepositAddress[collateral]; if (collateral == XEVT) { uint256 amountOut = transmuter.swapExactInput( @@ -207,8 +221,10 @@ contract MultiBlockRebalancer is AccessControl { address(this), block.timestamp ); - _adjustAllowance(collateral, address(depositAddresss), amountOut); - (uint256 shares, ) = IPool(depositAddresss).deposit(amountOut, address(this)); + _checkSlippage(amount, amountOut, collateral, depositAddress); + _adjustAllowance(collateral, address(depositAddress), amountOut); + (uint256 shares, ) = IPool(depositAddress).deposit(amountOut, address(this)); + _adjustAllowance(collateral, address(transmuter), shares); amountOut = transmuter.swapExactInput( shares, 0, @@ -227,11 +243,10 @@ contract MultiBlockRebalancer is AccessControl { address(this), block.timestamp ); - _adjustAllowance(collateral, address(depositAddresss), amountOut); - IERC20(collateral).transfer(depositAddresss, amountOut); + _checkSlippage(amount, amountOut, collateral, depositAddress); + IERC20(collateral).transfer(depositAddress, amountOut); } } else { - _adjustAllowance(address(agToken), address(transmuter), amount); uint256 amountOut = transmuter.swapExactInput( amount, 0, @@ -240,12 +255,13 @@ contract MultiBlockRebalancer is AccessControl { address(this), block.timestamp ); - address depositAddresss = collateralToDepositAddress[collateral]; + address depositAddress = collateralToDepositAddress[collateral]; + _checkSlippage(amount, amountOut, collateral, depositAddress); if (collateral == XEVT) { - IPool(depositAddresss).requestRedeem(amountOut); + IPool(depositAddress).requestRedeem(amountOut); } else if (collateral == USDM) { - IERC20(collateral).transfer(depositAddresss, amountOut); + IERC20(collateral).transfer(depositAddress, amountOut); } } } @@ -264,6 +280,11 @@ contract MultiBlockRebalancer is AccessControl { else collatInfo.minExposureYieldAsset = xFeeBurn[length - 2]; } + function _setMaxSlippage(uint96 newMaxSlippage) internal virtual { + if (newMaxSlippage > 1e9) revert InvalidParam(); + maxSlippage = newMaxSlippage; + } + /*////////////////////////////////////////////////////////////////////////////////////////////////////////////////// HELPER //////////////////////////////////////////////////////////////////////////////////////////////////////////////////*/ @@ -272,4 +293,21 @@ contract MultiBlockRebalancer is AccessControl { uint256 allowance = IERC20(token).allowance(address(this), sender); if (allowance < amountIn) IERC20(token).safeIncreaseAllowance(sender, type(uint256).max - allowance); } + + function _checkSlippage( + uint256 amountIn, + uint256 amountOut, + address collateral, + address depositAddress + ) internal view { + if (collateral == USDC || collateral == USDM) { + // Assume 1:1 ratio between stablecoins + uint256 slippage = ((amountIn - amountOut) * 1e9) / amountIn; + if (slippage > maxSlippage) revert SlippageTooHigh(); + } else if (collateral == XEVT) { + // Assumer 1:1 ratio between the underlying asset of the vault + uint256 slippage = ((IPool(depositAddress).convertToAssets(amountOut) - amountIn) * 1e9) / amountIn; + if (slippage > maxSlippage) revert SlippageTooHigh(); + } else revert InvalidParam(); + } } diff --git a/contracts/interfaces/IPool.sol b/contracts/interfaces/IPool.sol index 80cf608b..a9bc9bf0 100644 --- a/contracts/interfaces/IPool.sol +++ b/contracts/interfaces/IPool.sol @@ -6,4 +6,8 @@ interface IPool { function deposit(uint256 assets, address lender) external returns (uint256 shares, uint256 transferInDayTimestamp); function requestRedeem(uint256 shares) external returns (uint256 assets); + + function convertToAssets(uint256 shares) external view returns (uint256 assets); + + function convertToShares(uint256 assets) external view returns (uint256 shares); } From 919971a3ca7be974e6450b9c50587678ac63e586 Mon Sep 17 00:00:00 2001 From: 0xtekgrinder <0xtekgrinder@protonmail.com> Date: Fri, 20 Sep 2024 12:12:09 +0200 Subject: [PATCH 08/69] feat: remove old harvesters --- contracts/helpers/BaseHarvester.sol | 202 ----- contracts/helpers/BaseRebalancerFlashloan.sol | 113 --- contracts/helpers/HarvesterSwap.sol | 33 - contracts/helpers/HarvesterVault.sol | 50 -- contracts/helpers/Rebalancer.sol | 209 ----- contracts/helpers/RebalancerFlashloanSwap.sol | 94 --- .../helpers/RebalancerFlashloanVault.sol | 41 - wow | 769 ++++++++++++++++++ 8 files changed, 769 insertions(+), 742 deletions(-) delete mode 100644 contracts/helpers/BaseHarvester.sol delete mode 100644 contracts/helpers/BaseRebalancerFlashloan.sol delete mode 100644 contracts/helpers/HarvesterSwap.sol delete mode 100644 contracts/helpers/HarvesterVault.sol delete mode 100644 contracts/helpers/Rebalancer.sol delete mode 100644 contracts/helpers/RebalancerFlashloanSwap.sol delete mode 100644 contracts/helpers/RebalancerFlashloanVault.sol create mode 100644 wow diff --git a/contracts/helpers/BaseHarvester.sol b/contracts/helpers/BaseHarvester.sol deleted file mode 100644 index 4196c82f..00000000 --- a/contracts/helpers/BaseHarvester.sol +++ /dev/null @@ -1,202 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0 - -pragma solidity ^0.8.23; - -import { SafeCast } from "oz/utils/math/SafeCast.sol"; - -import { ITransmuter } from "interfaces/ITransmuter.sol"; - -import { AccessControl, IAccessControlManager } from "../utils/AccessControl.sol"; -import "../utils/Constants.sol"; -import "../utils/Errors.sol"; - -import { IRebalancerFlashloan } from "../interfaces/IRebalancerFlashloan.sol"; - -struct CollatParams { - // 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 asset - uint64 maxExposureYieldAsset; - // 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 BaseHarvester -/// @author Angle Labs, Inc. -/// @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 - IRebalancerFlashloan public rebalancer; - /// @notice Max slippage when dealing with the Transmuter - uint96 public maxSlippage; - /// @notice Data associated to a collateral - mapping(address => CollatParams) public collateralData; - - /*////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - INITIALIZATION - //////////////////////////////////////////////////////////////////////////////////////////////////////////////////*/ - - constructor( - address _rebalancer, - address collateral, - address asset, - uint64 targetExposure, - uint64 overrideExposures, - uint64 maxExposureYieldAsset, - uint64 minExposureYieldAsset, - uint96 _maxSlippage - ) { - ITransmuter transmuter = IRebalancerFlashloan(_rebalancer).TRANSMUTER(); - TRANSMUTER = transmuter; - rebalancer = IRebalancerFlashloan(_rebalancer); - accessControlManager = IAccessControlManager(transmuter.accessControlManager()); - _setCollateralData( - collateral, - asset, - targetExposure, - minExposureYieldAsset, - maxExposureYieldAsset, - overrideExposures - ); - _setMaxSlippage(_maxSlippage); - } - - /*////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - HARVEST - //////////////////////////////////////////////////////////////////////////////////////////////////////////////////*/ - - /// @notice Invests or divests from the yield asset associated to `collateral` based on the current exposure to this - /// collateral - /// @dev This transaction either reduces the exposure to `collateral` in the Transmuter or frees up some collateral - /// 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 - 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 stablecoinsFromAsset, ) = TRANSMUTER.getIssuedByCollateral(collatInfo.asset); - uint8 increase; - uint256 amount; - uint256 targetExposureScaled = collatInfo.targetExposure * stablecoinsIssued; - if (stablecoinsFromCollateral * 1e9 > targetExposureScaled) { - // Need to increase exposure to yield bearing asset - increase = 1; - amount = stablecoinsFromCollateral - targetExposureScaled / 1e9; - uint256 maxValueScaled = collatInfo.maxExposureYieldAsset * stablecoinsIssued; - // These checks assume that there are no transaction fees on the stablecoin->collateral conversion and so - // it's still possible that exposure goes above the max exposure in some rare cases - 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 (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.asset) {} catch {} - - rebalancer.adjustYieldExposure( - amount, - increase, - collateral, - collatInfo.asset, - (amount * (1e9 - maxSlippage)) / 1e9, - extraData - ); - } - } - - /*////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - SETTERS - //////////////////////////////////////////////////////////////////////////////////////////////////////////////////*/ - - function setRebalancer(address _newRebalancer) public virtual onlyGuardian { - if (_newRebalancer == address(0)) revert ZeroAddress(); - rebalancer = IRebalancerFlashloan(_newRebalancer); - } - - function setCollateralData( - address collateral, - address asset, - uint64 targetExposure, - uint64 minExposureYieldAsset, - uint64 maxExposureYieldAsset, - uint64 overrideExposures - ) public virtual onlyGuardian { - _setCollateralData( - collateral, - asset, - targetExposure, - minExposureYieldAsset, - maxExposureYieldAsset, - overrideExposures - ); - } - - function setMaxSlippage(uint96 _maxSlippage) public virtual onlyGuardian { - _setMaxSlippage(_maxSlippage); - } - - function updateLimitExposuresYieldAsset(address collateral) public virtual { - CollatParams storage collatInfo = collateralData[collateral]; - if (collatInfo.overrideExposures == 2) _updateLimitExposuresYieldAsset(collatInfo); - } - - function _setMaxSlippage(uint96 _maxSlippage) internal virtual { - if (_maxSlippage > 1e9) revert InvalidParam(); - maxSlippage = _maxSlippage; - } - - function _setCollateralData( - address collateral, - address asset, - uint64 targetExposure, - uint64 minExposureYieldAsset, - uint64 maxExposureYieldAsset, - uint64 overrideExposures - ) internal virtual { - CollatParams storage collatInfo = collateralData[collateral]; - collatInfo.asset = asset; - if (targetExposure >= 1e9) revert InvalidParam(); - collatInfo.targetExposure = targetExposure; - collatInfo.overrideExposures = overrideExposures; - if (overrideExposures == 1) { - if (maxExposureYieldAsset >= 1e9 || minExposureYieldAsset >= maxExposureYieldAsset) revert InvalidParam(); - collatInfo.maxExposureYieldAsset = maxExposureYieldAsset; - collatInfo.minExposureYieldAsset = minExposureYieldAsset; - } else { - collatInfo.overrideExposures = 2; - _updateLimitExposuresYieldAsset(collatInfo); - } - } - - function _updateLimitExposuresYieldAsset(CollatParams storage collatInfo) internal virtual { - uint64[] memory xFeeMint; - (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.asset); - length = xFeeBurn.length; - if (length <= 1) collatInfo.minExposureYieldAsset = 0; - else collatInfo.minExposureYieldAsset = xFeeBurn[length - 2]; - } -} diff --git a/contracts/helpers/BaseRebalancerFlashloan.sol b/contracts/helpers/BaseRebalancerFlashloan.sol deleted file mode 100644 index d9f461fb..00000000 --- a/contracts/helpers/BaseRebalancerFlashloan.sol +++ /dev/null @@ -1,113 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0 - -pragma solidity ^0.8.19; - -import "./Rebalancer.sol"; -import { IERC3156FlashBorrower } from "oz/interfaces/IERC3156FlashBorrower.sol"; -import { IERC3156FlashLender } from "oz/interfaces/IERC3156FlashLender.sol"; - -/// @title BaseRebalancerFlashloan -/// @author Angle Labs, Inc. -/// @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"); - - /// @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, 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, 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 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, asset, minAmountOut, extraData) - ); - } - - /// @inheritdoc IERC3156FlashBorrower - function onFlashLoan( - address initiator, - address, - uint256 amount, - uint256 fee, - bytes calldata data - ) public virtual returns (bytes32) { - if (msg.sender != address(FLASHLOAN) || initiator != address(this) || fee != 0) revert NotTrusted(); - (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 yield bearing asset - tokenOut = collateral; - tokenIn = asset; - } else { - // Decrease yield exposure action: we bring in the liquid asset - tokenIn = collateral; - tokenOut = asset; - } - uint256 amountOut = TRANSMUTER.swapExactInput(amount, 0, AGTOKEN, tokenOut, address(this), block.timestamp); - - // Swap to tokenIn - amountOut = _swapToTokenIn(typeAction, tokenIn, tokenOut, amountOut, callData); - - _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; - } - - /** - * @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 deleted file mode 100644 index 3ad3d151..00000000 --- a/contracts/helpers/HarvesterSwap.sol +++ /dev/null @@ -1,33 +0,0 @@ -// 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 deleted file mode 100644 index effce3ef..00000000 --- a/contracts/helpers/HarvesterVault.sol +++ /dev/null @@ -1,50 +0,0 @@ -// 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/Rebalancer.sol b/contracts/helpers/Rebalancer.sol deleted file mode 100644 index 2545231a..00000000 --- a/contracts/helpers/Rebalancer.sol +++ /dev/null @@ -1,209 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0 - -pragma solidity ^0.8.19; - -import { IERC20 } from "oz/interfaces/IERC20.sol"; -import { SafeERC20 } from "oz/token/ERC20/utils/SafeERC20.sol"; -import { SafeCast } from "oz/utils/math/SafeCast.sol"; - -import { ITransmuter } from "interfaces/ITransmuter.sol"; -import { Order, IRebalancer } from "interfaces/IRebalancer.sol"; - -import { AccessControl, IAccessControlManager } from "../utils/AccessControl.sol"; -import "../utils/Constants.sol"; -import "../utils/Errors.sol"; - -/// @title Rebalancer -/// @author Angle Labs, Inc. -/// @notice Contract built to subsidize rebalances between collateral tokens -/// @dev This contract is meant to "wrap" the Transmuter contract and provide a way for governance to -/// subsidize rebalances between collateral tokens. Rebalances are done through 2 swaps collateral <> agToken. -/// @dev This contract is not meant to hold any transient funds aside from the rebalancing budget -contract Rebalancer is IRebalancer, AccessControl { - event OrderSet(address indexed tokenIn, address indexed tokenOut, uint256 subsidyBudget, uint256 guaranteedRate); - event SubsidyPaid(address indexed tokenIn, address indexed tokenOut, uint256 subsidy); - - using SafeERC20 for IERC20; - using SafeCast for uint256; - - /// @notice Reference to the `transmuter` implementation this contract aims at rebalancing - ITransmuter public immutable TRANSMUTER; - /// @notice AgToken handled by the `transmuter` of interest - address public immutable AGTOKEN; - /// @notice Maps a `(tokenIn,tokenOut)` pair to details about the subsidy potentially provided on - /// `tokenIn` to `tokenOut` rebalances - mapping(address tokenIn => mapping(address tokenOut => Order)) public orders; - /// @notice Gives the total subsidy budget - uint256 public budget; - - /*////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - INITIALIZATION - //////////////////////////////////////////////////////////////////////////////////////////////////////////////////*/ - - /// @notice Initializes the immutable variables of the contract: `accessControlManager`, `transmuter` and `agToken` - constructor(IAccessControlManager _accessControlManager, ITransmuter _transmuter) { - if (address(_accessControlManager) == address(0)) revert ZeroAddress(); - accessControlManager = _accessControlManager; - TRANSMUTER = _transmuter; - AGTOKEN = address(_transmuter.agToken()); - } - - /*////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - REBALANCING FUNCTIONS - //////////////////////////////////////////////////////////////////////////////////////////////////////////////////*/ - - /// @inheritdoc IRebalancer - /// @dev Contrarily to what is done in the Transmuter contract, here neither of `tokenIn` or `tokenOut` - /// should be an `agToken` - /// @dev Can be used even if the subsidy budget is 0, in which case it'll just do 2 Transmuter swaps - /// @dev The invariant should be that `msg.sender` injects `amountIn` in the transmuter and either the - /// subsidy is 0 either they receive a subsidy from this contract on top of the output Transmuter up to - /// the guaranteed amount out - function swapExactInput( - uint256 amountIn, - uint256 amountOutMin, - address tokenIn, - address tokenOut, - address to, - uint256 deadline - ) external returns (uint256 amountOut) { - 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 - _adjustAllowance(tokenIn, address(TRANSMUTER), amountIn); - // Mint agToken from `tokenIn` - uint256 amountAgToken = TRANSMUTER.swapExactInput( - amountIn, - 0, - tokenIn, - AGTOKEN, - address(this), - block.timestamp - ); - // Computing if a potential subsidy must be included in the agToken amount to burn - uint256 subsidy = _getSubsidyAmount(tokenIn, tokenOut, amountAgToken, amountIn); - if (subsidy > 0) { - orders[tokenIn][tokenOut].subsidyBudget -= subsidy.toUint112(); - budget -= subsidy; - amountAgToken += subsidy; - - emit SubsidyPaid(tokenIn, tokenOut, subsidy); - } - amountOut = TRANSMUTER.swapExactInput(amountAgToken, amountOutMin, AGTOKEN, tokenOut, to, deadline); - } - - /// @inheritdoc IRebalancer - /// @dev This function returns an approximation and not an exact value as the first mint to compute `amountAgToken` - /// might change the state of the fees slope within the Transmuter that will then be taken into account when - /// burning the minted agToken. - function quoteIn(uint256 amountIn, address tokenIn, address tokenOut) external view returns (uint256 amountOut) { - uint256 amountAgToken = TRANSMUTER.quoteIn(amountIn, tokenIn, AGTOKEN); - amountAgToken += _getSubsidyAmount(tokenIn, tokenOut, amountAgToken, amountIn); - amountOut = TRANSMUTER.quoteIn(amountAgToken, AGTOKEN, tokenOut); - } - - /// @inheritdoc IRebalancer - /// @dev Note that this minimum amount is guaranteed up to the subsidy budget, and if for a swap the subsidy budget - /// is not big enough to provide this guaranteed amount out, then less will actually be obtained - function getGuaranteedAmountOut( - address tokenIn, - address tokenOut, - uint256 amountIn - ) external view returns (uint256) { - Order storage order = orders[tokenIn][tokenOut]; - return _getGuaranteedAmountOut(amountIn, order.guaranteedRate, order.decimalsIn, order.decimalsOut); - } - - /// @notice Internal version of `_getGuaranteedAmountOut` - function _getGuaranteedAmountOut( - uint256 amountIn, - uint256 guaranteedRate, - uint8 decimalsIn, - uint8 decimalsOut - ) internal pure returns (uint256 amountOut) { - return (amountIn * guaranteedRate * (10 ** decimalsOut)) / (BASE_18 * (10 ** decimalsIn)); - } - - /// @notice Computes the additional subsidy amount in agToken that must be added during the process of a swap - /// of `amountIn` of `tokenIn` to `tokenOut` - function _getSubsidyAmount( - address tokenIn, - address tokenOut, - uint256 amountAgToken, - uint256 amountIn - ) internal view returns (uint256 subsidy) { - Order storage order = orders[tokenIn][tokenOut]; - uint256 guaranteedAmountOut = _getGuaranteedAmountOut( - amountIn, - order.guaranteedRate, - order.decimalsIn, - order.decimalsOut - ); - // Computing the amount of agToken that must be burnt to get the amountOut guaranteed - if (guaranteedAmountOut > 0) { - uint256 amountAgTokenNeeded = TRANSMUTER.quoteOut(guaranteedAmountOut, AGTOKEN, tokenOut); - // If more agTokens than what has been obtained through the first mint must be burnt to get to the - // guaranteed amountOut, we're taking it from the subsidy budget set - if (amountAgToken < amountAgTokenNeeded) { - subsidy = amountAgTokenNeeded - amountAgToken; - - // In the case where the subsidy budget is too small, we may not be able to provide the guaranteed - // amountOut to the user - if (subsidy > order.subsidyBudget) subsidy = order.subsidyBudget; - } - } - } - - /*////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - GOVERNANCE - //////////////////////////////////////////////////////////////////////////////////////////////////////////////////*/ - - /// @inheritdoc IRebalancer - /// @dev Before calling this function, governance must make sure that there are enough `agToken` idle - /// in the contract to sponsor the swaps - /// @dev This function can be used to decrease an order by overriding it - function setOrder( - address tokenIn, - address tokenOut, - uint256 subsidyBudget, - uint256 guaranteedRate - ) external onlyGuardian { - Order storage order = orders[tokenIn][tokenOut]; - uint8 decimalsIn = order.decimalsIn; - uint8 decimalsOut = order.decimalsOut; - if (decimalsIn == 0) { - decimalsIn = TRANSMUTER.getCollateralDecimals(tokenIn); - order.decimalsIn = decimalsIn; - } - if (decimalsOut == 0) { - decimalsOut = TRANSMUTER.getCollateralDecimals(tokenOut); - order.decimalsOut = decimalsOut; - } - // If a token has 0 decimals on the Transmuter, then it's not an actual collateral of the Transmuter - if (decimalsIn == 0 || decimalsOut == 0) revert NotCollateral(); - uint256 newBudget = budget + subsidyBudget - order.subsidyBudget; - if (IERC20(AGTOKEN).balanceOf(address(this)) < newBudget) revert InvalidParam(); - budget = newBudget; - order.subsidyBudget = subsidyBudget.toUint112(); - order.guaranteedRate = guaranteedRate.toUint128(); - - emit OrderSet(tokenIn, tokenOut, subsidyBudget, guaranteedRate); - } - - /// @inheritdoc IRebalancer - /// @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 onlyGuardian { - if (token == address(AGTOKEN) && IERC20(token).balanceOf(address(this)) < budget + amount) - 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/RebalancerFlashloanSwap.sol b/contracts/helpers/RebalancerFlashloanSwap.sol deleted file mode 100644 index cfa19111..00000000 --- a/contracts/helpers/RebalancerFlashloanSwap.sol +++ /dev/null @@ -1,94 +0,0 @@ -// 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 deleted file mode 100644 index 1a696f46..00000000 --- a/contracts/helpers/RebalancerFlashloanVault.sol +++ /dev/null @@ -1,41 +0,0 @@ -// 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/wow b/wow new file mode 100644 index 00000000..090b6730 --- /dev/null +++ b/wow @@ -0,0 +1,769 @@ +Compiling 8 files with Solc 0.8.23 +Solc 0.8.23 finished in 143.42ms +Compiler run successful! +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0 ^0.8.1 ^0.8.2; + +// lib/openzeppelin-contracts/contracts/interfaces/draft-IERC1822.sol + +// OpenZeppelin Contracts (last updated v4.5.0) (interfaces/draft-IERC1822.sol) + +/** + * @dev ERC1822: Universal Upgradeable Proxy Standard (UUPS) documents a method for upgradeability through a simplified + * proxy whose upgrades are fully controlled by the current implementation. + */ +interface IERC1822Proxiable { + /** + * @dev Returns the storage slot that the proxiable contract assumes is being used to store the implementation + * address. + * + * IMPORTANT: A proxy pointing at a proxiable contract should not be considered proxiable itself, because this risks + * bricking a proxy that upgrades to it, by delegating to itself until out of gas. Thus it is critical that this + * function revert if invoked through a proxy. + */ + function proxiableUUID() external view returns (bytes32); +} + +// lib/openzeppelin-contracts/contracts/proxy/Proxy.sol + +// OpenZeppelin Contracts (last updated v4.6.0) (proxy/Proxy.sol) + +/** + * @dev This abstract contract provides a fallback function that delegates all calls to another contract using the EVM + * instruction `delegatecall`. We refer to the second contract as the _implementation_ behind the proxy, and it has to + * be specified by overriding the virtual {_implementation} function. + * + * Additionally, delegation to the implementation can be triggered manually through the {_fallback} function, or to a + * different contract through the {_delegate} function. + * + * The success and return data of the delegated call will be returned back to the caller of the proxy. + */ +abstract contract Proxy { + /** + * @dev Delegates the current call to `implementation`. + * + * This function does not return to its internal call site, it will return directly to the external caller. + */ + function _delegate(address implementation) internal virtual { + assembly { + // Copy msg.data. We take full control of memory in this inline assembly + // block because it will not return to Solidity code. We overwrite the + // Solidity scratch pad at memory position 0. + calldatacopy(0, 0, calldatasize()) + + // Call the implementation. + // out and outsize are 0 because we don't know the size yet. + let result := delegatecall(gas(), implementation, 0, calldatasize(), 0, 0) + + // Copy the returned data. + returndatacopy(0, 0, returndatasize()) + + switch result + // delegatecall returns 0 on error. + case 0 { + revert(0, returndatasize()) + } + default { + return(0, returndatasize()) + } + } + } + + /** + * @dev This is a virtual function that should be overridden so it returns the address to which the fallback function + * and {_fallback} should delegate. + */ + function _implementation() internal view virtual returns (address); + + /** + * @dev Delegates the current call to the address returned by `_implementation()`. + * + * This function does not return to its internal call site, it will return directly to the external caller. + */ + function _fallback() internal virtual { + _beforeFallback(); + _delegate(_implementation()); + } + + /** + * @dev Fallback function that delegates calls to the address returned by `_implementation()`. Will run if no other + * function in the contract matches the call data. + */ + fallback() external payable virtual { + _fallback(); + } + + /** + * @dev Fallback function that delegates calls to the address returned by `_implementation()`. Will run if call data + * is empty. + */ + receive() external payable virtual { + _fallback(); + } + + /** + * @dev Hook that is called before falling back to the implementation. Can happen as part of a manual `_fallback` + * call, or as part of the Solidity `fallback` or `receive` functions. + * + * If overridden should call `super._beforeFallback()`. + */ + function _beforeFallback() internal virtual {} +} + +// lib/openzeppelin-contracts/contracts/proxy/beacon/IBeacon.sol + +// OpenZeppelin Contracts v4.4.1 (proxy/beacon/IBeacon.sol) + +/** + * @dev This is the interface that {BeaconProxy} expects of its beacon. + */ +interface IBeacon { + /** + * @dev Must return an address that can be used as a delegate call target. + * + * {BeaconProxy} will check that this address is a contract. + */ + function implementation() external view returns (address); +} + +// lib/openzeppelin-contracts/contracts/utils/Address.sol + +// OpenZeppelin Contracts (last updated v4.7.0) (utils/Address.sol) + +/** + * @dev Collection of functions related to the address type + */ +library Address { + /** + * @dev Returns true if `account` is a contract. + * + * [IMPORTANT] + * ==== + * It is unsafe to assume that an address for which this function returns + * false is an externally-owned account (EOA) and not a contract. + * + * Among others, `isContract` will return false for the following + * types of addresses: + * + * - an externally-owned account + * - a contract in construction + * - an address where a contract will be created + * - an address where a contract lived, but was destroyed + * ==== + * + * [IMPORTANT] + * ==== + * You shouldn't rely on `isContract` to protect against flash loan attacks! + * + * Preventing calls from contracts is highly discouraged. It breaks composability, breaks support for smart wallets + * like Gnosis Safe, and does not provide security since it can be circumvented by calling from a contract + * constructor. + * ==== + */ + function isContract(address account) internal view returns (bool) { + // This method relies on extcodesize/address.code.length, which returns 0 + // for contracts in construction, since the code is only stored at the end + // of the constructor execution. + + return account.code.length > 0; + } + + /** + * @dev Replacement for Solidity's `transfer`: sends `amount` wei to + * `recipient`, forwarding all available gas and reverting on errors. + * + * https://eips.ethereum.org/EIPS/eip-1884[EIP1884] increases the gas cost + * of certain opcodes, possibly making contracts go over the 2300 gas limit + * imposed by `transfer`, making them unable to receive funds via + * `transfer`. {sendValue} removes this limitation. + * + * https://diligence.consensys.net/posts/2019/09/stop-using-soliditys-transfer-now/[Learn more]. + * + * IMPORTANT: because control is transferred to `recipient`, care must be + * taken to not create reentrancy vulnerabilities. Consider using + * {ReentrancyGuard} or the + * https://solidity.readthedocs.io/en/v0.5.11/security-considerations.html#use-the-checks-effects-interactions-pattern[checks-effects-interactions pattern]. + */ + function sendValue(address payable recipient, uint256 amount) internal { + require(address(this).balance >= amount, "Address: insufficient balance"); + + (bool success, ) = recipient.call{value: amount}(""); + require(success, "Address: unable to send value, recipient may have reverted"); + } + + /** + * @dev Performs a Solidity function call using a low level `call`. A + * plain `call` is an unsafe replacement for a function call: use this + * function instead. + * + * If `target` reverts with a revert reason, it is bubbled up by this + * function (like regular Solidity function calls). + * + * Returns the raw returned data. To convert to the expected return value, + * use https://solidity.readthedocs.io/en/latest/units-and-global-variables.html?highlight=abi.decode#abi-encoding-and-decoding-functions[`abi.decode`]. + * + * Requirements: + * + * - `target` must be a contract. + * - calling `target` with `data` must not revert. + * + * _Available since v3.1._ + */ + function functionCall(address target, bytes memory data) internal returns (bytes memory) { + return functionCall(target, data, "Address: low-level call failed"); + } + + /** + * @dev Same as {xref-Address-functionCall-address-bytes-}[`functionCall`], but with + * `errorMessage` as a fallback revert reason when `target` reverts. + * + * _Available since v3.1._ + */ + function functionCall( + address target, + bytes memory data, + string memory errorMessage + ) internal returns (bytes memory) { + return functionCallWithValue(target, data, 0, errorMessage); + } + + /** + * @dev Same as {xref-Address-functionCall-address-bytes-}[`functionCall`], + * but also transferring `value` wei to `target`. + * + * Requirements: + * + * - the calling contract must have an ETH balance of at least `value`. + * - the called Solidity function must be `payable`. + * + * _Available since v3.1._ + */ + function functionCallWithValue( + address target, + bytes memory data, + uint256 value + ) internal returns (bytes memory) { + return functionCallWithValue(target, data, value, "Address: low-level call with value failed"); + } + + /** + * @dev Same as {xref-Address-functionCallWithValue-address-bytes-uint256-}[`functionCallWithValue`], but + * with `errorMessage` as a fallback revert reason when `target` reverts. + * + * _Available since v3.1._ + */ + function functionCallWithValue( + address target, + bytes memory data, + uint256 value, + string memory errorMessage + ) internal returns (bytes memory) { + require(address(this).balance >= value, "Address: insufficient balance for call"); + require(isContract(target), "Address: call to non-contract"); + + (bool success, bytes memory returndata) = target.call{value: value}(data); + return verifyCallResult(success, returndata, errorMessage); + } + + /** + * @dev Same as {xref-Address-functionCall-address-bytes-}[`functionCall`], + * but performing a static call. + * + * _Available since v3.3._ + */ + function functionStaticCall(address target, bytes memory data) internal view returns (bytes memory) { + return functionStaticCall(target, data, "Address: low-level static call failed"); + } + + /** + * @dev Same as {xref-Address-functionCall-address-bytes-string-}[`functionCall`], + * but performing a static call. + * + * _Available since v3.3._ + */ + function functionStaticCall( + address target, + bytes memory data, + string memory errorMessage + ) internal view returns (bytes memory) { + require(isContract(target), "Address: static call to non-contract"); + + (bool success, bytes memory returndata) = target.staticcall(data); + return verifyCallResult(success, returndata, errorMessage); + } + + /** + * @dev Same as {xref-Address-functionCall-address-bytes-}[`functionCall`], + * but performing a delegate call. + * + * _Available since v3.4._ + */ + function functionDelegateCall(address target, bytes memory data) internal returns (bytes memory) { + return functionDelegateCall(target, data, "Address: low-level delegate call failed"); + } + + /** + * @dev Same as {xref-Address-functionCall-address-bytes-string-}[`functionCall`], + * but performing a delegate call. + * + * _Available since v3.4._ + */ + function functionDelegateCall( + address target, + bytes memory data, + string memory errorMessage + ) internal returns (bytes memory) { + require(isContract(target), "Address: delegate call to non-contract"); + + (bool success, bytes memory returndata) = target.delegatecall(data); + return verifyCallResult(success, returndata, errorMessage); + } + + /** + * @dev Tool to verifies that a low level call was successful, and revert if it wasn't, either by bubbling the + * revert reason using the provided one. + * + * _Available since v4.3._ + */ + function verifyCallResult( + bool success, + bytes memory returndata, + string memory errorMessage + ) internal pure returns (bytes memory) { + if (success) { + return returndata; + } else { + // Look for revert reason and bubble it up if present + if (returndata.length > 0) { + // The easiest way to bubble the revert reason is using memory via assembly + /// @solidity memory-safe-assembly + assembly { + let returndata_size := mload(returndata) + revert(add(32, returndata), returndata_size) + } + } else { + revert(errorMessage); + } + } + } +} + +// lib/openzeppelin-contracts/contracts/utils/StorageSlot.sol + +// OpenZeppelin Contracts (last updated v4.7.0) (utils/StorageSlot.sol) + +/** + * @dev Library for reading and writing primitive types to specific storage slots. + * + * Storage slots are often used to avoid storage conflict when dealing with upgradeable contracts. + * This library helps with reading and writing to such slots without the need for inline assembly. + * + * The functions in this library return Slot structs that contain a `value` member that can be used to read or write. + * + * Example usage to set ERC1967 implementation slot: + * ``` + * contract ERC1967 { + * bytes32 internal constant _IMPLEMENTATION_SLOT = 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc; + * + * function _getImplementation() internal view returns (address) { + * return StorageSlot.getAddressSlot(_IMPLEMENTATION_SLOT).value; + * } + * + * function _setImplementation(address newImplementation) internal { + * require(Address.isContract(newImplementation), "ERC1967: new implementation is not a contract"); + * StorageSlot.getAddressSlot(_IMPLEMENTATION_SLOT).value = newImplementation; + * } + * } + * ``` + * + * _Available since v4.1 for `address`, `bool`, `bytes32`, and `uint256`._ + */ +library StorageSlot { + struct AddressSlot { + address value; + } + + struct BooleanSlot { + bool value; + } + + struct Bytes32Slot { + bytes32 value; + } + + struct Uint256Slot { + uint256 value; + } + + /** + * @dev Returns an `AddressSlot` with member `value` located at `slot`. + */ + function getAddressSlot(bytes32 slot) internal pure returns (AddressSlot storage r) { + /// @solidity memory-safe-assembly + assembly { + r.slot := slot + } + } + + /** + * @dev Returns an `BooleanSlot` with member `value` located at `slot`. + */ + function getBooleanSlot(bytes32 slot) internal pure returns (BooleanSlot storage r) { + /// @solidity memory-safe-assembly + assembly { + r.slot := slot + } + } + + /** + * @dev Returns an `Bytes32Slot` with member `value` located at `slot`. + */ + function getBytes32Slot(bytes32 slot) internal pure returns (Bytes32Slot storage r) { + /// @solidity memory-safe-assembly + assembly { + r.slot := slot + } + } + + /** + * @dev Returns an `Uint256Slot` with member `value` located at `slot`. + */ + function getUint256Slot(bytes32 slot) internal pure returns (Uint256Slot storage r) { + /// @solidity memory-safe-assembly + assembly { + r.slot := slot + } + } +} + +// lib/openzeppelin-contracts/contracts/proxy/ERC1967/ERC1967Upgrade.sol + +// OpenZeppelin Contracts (last updated v4.5.0) (proxy/ERC1967/ERC1967Upgrade.sol) + +/** + * @dev This abstract contract provides getters and event emitting update functions for + * https://eips.ethereum.org/EIPS/eip-1967[EIP1967] slots. + * + * _Available since v4.1._ + * + * @custom:oz-upgrades-unsafe-allow delegatecall + */ +abstract contract ERC1967Upgrade { + // This is the keccak-256 hash of "eip1967.proxy.rollback" subtracted by 1 + bytes32 private constant _ROLLBACK_SLOT = 0x4910fdfa16fed3260ed0e7147f7cc6da11a60208b5b9406d12a635614ffd9143; + + /** + * @dev Storage slot with the address of the current implementation. + * This is the keccak-256 hash of "eip1967.proxy.implementation" subtracted by 1, and is + * validated in the constructor. + */ + bytes32 internal constant _IMPLEMENTATION_SLOT = 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc; + + /** + * @dev Emitted when the implementation is upgraded. + */ + event Upgraded(address indexed implementation); + + /** + * @dev Returns the current implementation address. + */ + function _getImplementation() internal view returns (address) { + return StorageSlot.getAddressSlot(_IMPLEMENTATION_SLOT).value; + } + + /** + * @dev Stores a new address in the EIP1967 implementation slot. + */ + function _setImplementation(address newImplementation) private { + require(Address.isContract(newImplementation), "ERC1967: new implementation is not a contract"); + StorageSlot.getAddressSlot(_IMPLEMENTATION_SLOT).value = newImplementation; + } + + /** + * @dev Perform implementation upgrade + * + * Emits an {Upgraded} event. + */ + function _upgradeTo(address newImplementation) internal { + _setImplementation(newImplementation); + emit Upgraded(newImplementation); + } + + /** + * @dev Perform implementation upgrade with additional setup call. + * + * Emits an {Upgraded} event. + */ + function _upgradeToAndCall( + address newImplementation, + bytes memory data, + bool forceCall + ) internal { + _upgradeTo(newImplementation); + if (data.length > 0 || forceCall) { + Address.functionDelegateCall(newImplementation, data); + } + } + + /** + * @dev Perform implementation upgrade with security checks for UUPS proxies, and additional setup call. + * + * Emits an {Upgraded} event. + */ + function _upgradeToAndCallUUPS( + address newImplementation, + bytes memory data, + bool forceCall + ) internal { + // Upgrades from old implementations will perform a rollback test. This test requires the new + // implementation to upgrade back to the old, non-ERC1822 compliant, implementation. Removing + // this special case will break upgrade paths from old UUPS implementation to new ones. + if (StorageSlot.getBooleanSlot(_ROLLBACK_SLOT).value) { + _setImplementation(newImplementation); + } else { + try IERC1822Proxiable(newImplementation).proxiableUUID() returns (bytes32 slot) { + require(slot == _IMPLEMENTATION_SLOT, "ERC1967Upgrade: unsupported proxiableUUID"); + } catch { + revert("ERC1967Upgrade: new implementation is not UUPS"); + } + _upgradeToAndCall(newImplementation, data, forceCall); + } + } + + /** + * @dev Storage slot with the admin of the contract. + * This is the keccak-256 hash of "eip1967.proxy.admin" subtracted by 1, and is + * validated in the constructor. + */ + bytes32 internal constant _ADMIN_SLOT = 0xb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103; + + /** + * @dev Emitted when the admin account has changed. + */ + event AdminChanged(address previousAdmin, address newAdmin); + + /** + * @dev Returns the current admin. + */ + function _getAdmin() internal view returns (address) { + return StorageSlot.getAddressSlot(_ADMIN_SLOT).value; + } + + /** + * @dev Stores a new address in the EIP1967 admin slot. + */ + function _setAdmin(address newAdmin) private { + require(newAdmin != address(0), "ERC1967: new admin is the zero address"); + StorageSlot.getAddressSlot(_ADMIN_SLOT).value = newAdmin; + } + + /** + * @dev Changes the admin of the proxy. + * + * Emits an {AdminChanged} event. + */ + function _changeAdmin(address newAdmin) internal { + emit AdminChanged(_getAdmin(), newAdmin); + _setAdmin(newAdmin); + } + + /** + * @dev The storage slot of the UpgradeableBeacon contract which defines the implementation for this proxy. + * This is bytes32(uint256(keccak256('eip1967.proxy.beacon')) - 1)) and is validated in the constructor. + */ + bytes32 internal constant _BEACON_SLOT = 0xa3f0ad74e5423aebfd80d3ef4346578335a9a72aeaee59ff6cb3582b35133d50; + + /** + * @dev Emitted when the beacon is upgraded. + */ + event BeaconUpgraded(address indexed beacon); + + /** + * @dev Returns the current beacon. + */ + function _getBeacon() internal view returns (address) { + return StorageSlot.getAddressSlot(_BEACON_SLOT).value; + } + + /** + * @dev Stores a new beacon in the EIP1967 beacon slot. + */ + function _setBeacon(address newBeacon) private { + require(Address.isContract(newBeacon), "ERC1967: new beacon is not a contract"); + require( + Address.isContract(IBeacon(newBeacon).implementation()), + "ERC1967: beacon implementation is not a contract" + ); + StorageSlot.getAddressSlot(_BEACON_SLOT).value = newBeacon; + } + + /** + * @dev Perform beacon upgrade with additional setup call. Note: This upgrades the address of the beacon, it does + * not upgrade the implementation contained in the beacon (see {UpgradeableBeacon-_setImplementation} for that). + * + * Emits a {BeaconUpgraded} event. + */ + function _upgradeBeaconToAndCall( + address newBeacon, + bytes memory data, + bool forceCall + ) internal { + _setBeacon(newBeacon); + emit BeaconUpgraded(newBeacon); + if (data.length > 0 || forceCall) { + Address.functionDelegateCall(IBeacon(newBeacon).implementation(), data); + } + } +} + +// lib/openzeppelin-contracts/contracts/proxy/ERC1967/ERC1967Proxy.sol + +// OpenZeppelin Contracts (last updated v4.7.0) (proxy/ERC1967/ERC1967Proxy.sol) + +/** + * @dev This contract implements an upgradeable proxy. It is upgradeable because calls are delegated to an + * implementation address that can be changed. This address is stored in storage in the location specified by + * https://eips.ethereum.org/EIPS/eip-1967[EIP1967], so that it doesn't conflict with the storage layout of the + * implementation behind the proxy. + */ +contract ERC1967Proxy is Proxy, ERC1967Upgrade { + /** + * @dev Initializes the upgradeable proxy with an initial implementation specified by `_logic`. + * + * If `_data` is nonempty, it's used as data in a delegate call to `_logic`. This will typically be an encoded + * function call, and allows initializing the storage of the proxy like a Solidity constructor. + */ + constructor(address _logic, bytes memory _data) payable { + _upgradeToAndCall(_logic, _data, false); + } + + /** + * @dev Returns the current implementation address. + */ + function _implementation() internal view virtual override returns (address impl) { + return ERC1967Upgrade._getImplementation(); + } +} + +// lib/openzeppelin-contracts/contracts/proxy/transparent/TransparentUpgradeableProxy.sol + +// OpenZeppelin Contracts (last updated v4.7.0) (proxy/transparent/TransparentUpgradeableProxy.sol) + +/** + * @dev This contract implements a proxy that is upgradeable by an admin. + * + * To avoid https://medium.com/nomic-labs-blog/malicious-backdoors-in-ethereum-proxies-62629adf3357[proxy selector + * clashing], which can potentially be used in an attack, this contract uses the + * https://blog.openzeppelin.com/the-transparent-proxy-pattern/[transparent proxy pattern]. This pattern implies two + * things that go hand in hand: + * + * 1. If any account other than the admin calls the proxy, the call will be forwarded to the implementation, even if + * that call matches one of the admin functions exposed by the proxy itself. + * 2. If the admin calls the proxy, it can access the admin functions, but its calls will never be forwarded to the + * implementation. If the admin tries to call a function on the implementation it will fail with an error that says + * "admin cannot fallback to proxy target". + * + * These properties mean that the admin account can only be used for admin actions like upgrading the proxy or changing + * the admin, so it's best if it's a dedicated account that is not used for anything else. This will avoid headaches due + * to sudden errors when trying to call a function from the proxy implementation. + * + * Our recommendation is for the dedicated account to be an instance of the {ProxyAdmin} contract. If set up this way, + * you should think of the `ProxyAdmin` instance as the real administrative interface of your proxy. + */ +contract TransparentUpgradeableProxy is ERC1967Proxy { + /** + * @dev Initializes an upgradeable proxy managed by `_admin`, backed by the implementation at `_logic`, and + * optionally initialized with `_data` as explained in {ERC1967Proxy-constructor}. + */ + constructor( + address _logic, + address admin_, + bytes memory _data + ) payable ERC1967Proxy(_logic, _data) { + _changeAdmin(admin_); + } + + /** + * @dev Modifier used internally that will delegate the call to the implementation unless the sender is the admin. + */ + modifier ifAdmin() { + if (msg.sender == _getAdmin()) { + _; + } else { + _fallback(); + } + } + + /** + * @dev Returns the current admin. + * + * NOTE: Only the admin can call this function. See {ProxyAdmin-getProxyAdmin}. + * + * TIP: To get this value clients can read directly from the storage slot shown below (specified by EIP1967) using the + * https://eth.wiki/json-rpc/API#eth_getstorageat[`eth_getStorageAt`] RPC call. + * `0xb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103` + */ + function admin() external ifAdmin returns (address admin_) { + admin_ = _getAdmin(); + } + + /** + * @dev Returns the current implementation. + * + * NOTE: Only the admin can call this function. See {ProxyAdmin-getProxyImplementation}. + * + * TIP: To get this value clients can read directly from the storage slot shown below (specified by EIP1967) using the + * https://eth.wiki/json-rpc/API#eth_getstorageat[`eth_getStorageAt`] RPC call. + * `0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc` + */ + function implementation() external ifAdmin returns (address implementation_) { + implementation_ = _implementation(); + } + + /** + * @dev Changes the admin of the proxy. + * + * Emits an {AdminChanged} event. + * + * NOTE: Only the admin can call this function. See {ProxyAdmin-changeProxyAdmin}. + */ + function changeAdmin(address newAdmin) external virtual ifAdmin { + _changeAdmin(newAdmin); + } + + /** + * @dev Upgrade the implementation of the proxy. + * + * NOTE: Only the admin can call this function. See {ProxyAdmin-upgrade}. + */ + function upgradeTo(address newImplementation) external ifAdmin { + _upgradeToAndCall(newImplementation, bytes(""), false); + } + + /** + * @dev Upgrade the implementation of the proxy, and then call a function from the new implementation as specified + * by `data`, which should be an encoded function call. This is useful to initialize new storage variables in the + * proxied contract. + * + * NOTE: Only the admin can call this function. See {ProxyAdmin-upgradeAndCall}. + */ + function upgradeToAndCall(address newImplementation, bytes calldata data) external payable ifAdmin { + _upgradeToAndCall(newImplementation, data, true); + } + + /** + * @dev Returns the current admin. + */ + function _admin() internal view virtual returns (address) { + return _getAdmin(); + } + + /** + * @dev Makes sure the admin cannot access the fallback function. See {Proxy-_beforeFallback}. + */ + function _beforeFallback() internal virtual override { + require(msg.sender != _getAdmin(), "TransparentUpgradeableProxy: admin cannot fallback to proxy target"); + super._beforeFallback(); + } +} + From cd1e1b44f97b3e755f1cc6683616cbcc7d0f1ee3 Mon Sep 17 00:00:00 2001 From: 0xtekgrinder <0xtekgrinder@protonmail.com> Date: Fri, 20 Sep 2024 12:27:06 +0200 Subject: [PATCH 09/69] feat: correct slippage computation for the vault --- contracts/helpers/MultiBlockRebalancer.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/helpers/MultiBlockRebalancer.sol b/contracts/helpers/MultiBlockRebalancer.sol index 7fa883a8..d4e3ea2c 100644 --- a/contracts/helpers/MultiBlockRebalancer.sol +++ b/contracts/helpers/MultiBlockRebalancer.sol @@ -306,7 +306,7 @@ contract MultiBlockRebalancer is AccessControl { if (slippage > maxSlippage) revert SlippageTooHigh(); } else if (collateral == XEVT) { // Assumer 1:1 ratio between the underlying asset of the vault - uint256 slippage = ((IPool(depositAddress).convertToAssets(amountOut) - amountIn) * 1e9) / amountIn; + uint256 slippage = ((amountIn - IPool(depositAddress).convertToAssets(amountOut)) * 1e9) / amountIn; if (slippage > maxSlippage) revert SlippageTooHigh(); } else revert InvalidParam(); } From 737b96c7e696b3f70b5a0dc897647fac98d71ddd Mon Sep 17 00:00:00 2001 From: 0xtekgrinder <0xtekgrinder@protonmail.com> Date: Fri, 20 Sep 2024 14:56:59 +0200 Subject: [PATCH 10/69] feat: refactory harvesters to abstract common code --- contracts/helpers/BaseRebalancer.sol | 214 ++++++++++++++++++++ contracts/helpers/GenericHarvester.sol | 224 +++++---------------- contracts/helpers/MultiBlockRebalancer.sol | 130 +----------- test/utils/FunctionUtils.sol | 2 +- 4 files changed, 275 insertions(+), 295 deletions(-) create mode 100644 contracts/helpers/BaseRebalancer.sol diff --git a/contracts/helpers/BaseRebalancer.sol b/contracts/helpers/BaseRebalancer.sol new file mode 100644 index 00000000..ec6a39ca --- /dev/null +++ b/contracts/helpers/BaseRebalancer.sol @@ -0,0 +1,214 @@ +// SPDX-License-Identifier: GPL-3.0 + +pragma solidity ^0.8.23; + +import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import { AccessControl, IAccessControlManager } from "../utils/AccessControl.sol"; +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import { ITransmuter } from "../interfaces/ITransmuter.sol"; +import { IAgToken } from "../interfaces/IAgToken.sol"; + +import "../utils/Errors.sol"; + +struct CollatParams { + // Address of the collateral + address asset; + // Target exposure to the collateral asset used + uint64 targetExposure; + // Maximum exposure within the Transmuter to the asset + uint64 maxExposureYieldAsset; + // 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; +} + +abstract contract BaseRebalancer is AccessControl { + using SafeERC20 for IERC20; + + /*////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + VARIABLES + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////*/ + + /// @notice Reference to the `transmuter` implementation this contract aims at rebalancing + ITransmuter public immutable transmuter; + /// @notice AgToken handled by the `transmuter` of interest + IAgToken public immutable agToken; + /// @notice Max slippage when dealing with the Transmuter + uint96 public maxSlippage; + /// @notice Data associated to a collateral + mapping(address => CollatParams) public collateralData; + + /*////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + CONSTRUCTOR + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////*/ + + constructor( + uint96 initialMaxSlippage, + IAccessControlManager definitiveAccessControlManager, + IAgToken definitiveAgToken, + ITransmuter definitiveTransmuter + ) { + _setMaxSlippage(initialMaxSlippage); + accessControlManager = definitiveAccessControlManager; + agToken = definitiveAgToken; + transmuter = definitiveTransmuter; + } + /*////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + GUARDIAN FUNCTIONS + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////*/ + + /** + * @notice Set the collateral data + * @param collateral address of the collateral + * @param targetExposure target exposure to the collateral asset used + * @param minExposureYieldAsset minimum exposure within the Transmuter to the asset + * @param maxExposureYieldAsset maximum exposure within the Transmuter to the asset + * @param overrideExposures 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 + */ + function setCollateralData( + address collateral, + uint64 targetExposure, + uint64 minExposureYieldAsset, + uint64 maxExposureYieldAsset, + uint64 overrideExposures + ) external onlyGuardian { + _setCollateralData( + collateral, + collateral, + targetExposure, + minExposureYieldAsset, + maxExposureYieldAsset, + overrideExposures + ); + } + + /** + * @notice Set the collateral data + * @param collateral address of the collateral + * @param asset address of the asset + * @param targetExposure target exposure to the collateral asset used + * @param minExposureYieldAsset minimum exposure within the Transmuter to the asset + * @param maxExposureYieldAsset maximum exposure within the Transmuter to the asset + * @param overrideExposures whether limit exposures should be overriden or read onchain through the Transmuter + */ + function setCollateralData( + address collateral, + address asset, + uint64 targetExposure, + uint64 minExposureYieldAsset, + uint64 maxExposureYieldAsset, + uint64 overrideExposures + ) external onlyGuardian { + _setCollateralData( + collateral, + asset, + targetExposure, + minExposureYieldAsset, + maxExposureYieldAsset, + overrideExposures + ); + } + + /** + * @notice Set the limit exposures to the yield bearing asset + * @param collateral address of the collateral + */ + function updateLimitExposuresYieldAsset(address collateral) public virtual onlyGuardian { + CollatParams storage collatInfo = collateralData[collateral]; + if (collatInfo.overrideExposures == 2) _updateLimitExposuresYieldAsset(collatInfo); + } + + /** + * @notice Set the max allowed slippage + * @param newMaxSlippage new max allowed slippage + */ + function setMaxSlippage(uint96 newMaxSlippage) external onlyGuardian { + _setMaxSlippage(newMaxSlippage); + } + + /*////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + INTERNAL FUNCTIONS + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////*/ + + function _computeRebalanceAmount( + address collateral, + CollatParams memory collatInfo + ) internal view returns (uint8 increase, uint256 amount) { + (uint256 stablecoinsFromCollateral, uint256 stablecoinsIssued) = transmuter.getIssuedByCollateral(collateral); + (uint256 stablecoinsFromAsset, ) = transmuter.getIssuedByCollateral(collatInfo.asset); + uint256 targetExposureScaled = collatInfo.targetExposure * stablecoinsIssued; + if (stablecoinsFromCollateral * 1e9 > targetExposureScaled) { + // Need to increase exposure to yield bearing asset + increase = 1; + amount = stablecoinsFromCollateral - targetExposureScaled / 1e9; + uint256 maxValueScaled = collatInfo.maxExposureYieldAsset * stablecoinsIssued; + // These checks assume that there are no transaction fees on the stablecoin->collateral conversion and so + // it's still possible that exposure goes above the max exposure in some rare cases + 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 (stablecoinsFromAsset * 1e9 < minValueScaled) amount = 0; + else if (stablecoinsFromAsset * 1e9 < minValueScaled + amount * 1e9) + amount = stablecoinsFromAsset - minValueScaled / 1e9; + } + } + + function _setCollateralData( + address collateral, + address asset, + uint64 targetExposure, + uint64 minExposureYieldAsset, + uint64 maxExposureYieldAsset, + uint64 overrideExposures + ) internal virtual { + CollatParams storage collatInfo = collateralData[collateral]; + collatInfo.asset = asset; + if (targetExposure >= 1e9) revert InvalidParam(); + collatInfo.targetExposure = targetExposure; + collatInfo.overrideExposures = overrideExposures; + if (overrideExposures == 1) { + if (maxExposureYieldAsset >= 1e9 || minExposureYieldAsset >= maxExposureYieldAsset) revert InvalidParam(); + collatInfo.maxExposureYieldAsset = maxExposureYieldAsset; + collatInfo.minExposureYieldAsset = minExposureYieldAsset; + } else { + collatInfo.overrideExposures = 2; + _updateLimitExposuresYieldAsset(collatInfo); + } + } + + function _updateLimitExposuresYieldAsset(CollatParams storage collatInfo) internal virtual { + uint64[] memory xFeeMint; + (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.asset); + length = xFeeBurn.length; + if (length <= 1) collatInfo.minExposureYieldAsset = 0; + else collatInfo.minExposureYieldAsset = xFeeBurn[length - 2]; + } + + function _setMaxSlippage(uint96 newMaxSlippage) internal virtual { + if (newMaxSlippage > 1e9) revert InvalidParam(); + maxSlippage = newMaxSlippage; + } + + /*////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + 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/GenericHarvester.sol b/contracts/helpers/GenericHarvester.sol index bb8c144b..3a0293d1 100644 --- a/contracts/helpers/GenericHarvester.sol +++ b/contracts/helpers/GenericHarvester.sol @@ -8,6 +8,7 @@ import { IERC20 } from "oz/interfaces/IERC20.sol"; import { IERC20Metadata } from "oz/interfaces/IERC20Metadata.sol"; import { ITransmuter } from "interfaces/ITransmuter.sol"; +import { IAgToken } from "interfaces/IAgToken.sol"; import { RouterSwapper } from "utils/src/RouterSwapper.sol"; import { AccessControl, IAccessControlManager } from "../utils/AccessControl.sol"; @@ -17,20 +18,7 @@ import "../utils/Errors.sol"; import { IERC3156FlashBorrower } from "oz/interfaces/IERC3156FlashBorrower.sol"; import { IERC3156FlashLender } from "oz/interfaces/IERC3156FlashLender.sol"; import { IERC4626 } from "interfaces/external/IERC4626.sol"; - -struct CollatParams { - // 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 asset - uint64 maxExposureYieldAsset; - // 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; -} +import { BaseRebalancer, CollatParams } from "./BaseRebalancer.sol"; enum SwapType { VAULT, @@ -40,24 +28,16 @@ enum SwapType { /// @title GenericHarvester /// @author Angle Labs, Inc. /// @dev Generic contract for anyone to permissionlessly adjust the reserves of Angle Transmuter -contract GenericHarvester is AccessControl, IERC3156FlashBorrower, RouterSwapper { +contract GenericHarvester is BaseRebalancer, IERC3156FlashBorrower, RouterSwapper { using SafeCast for uint256; using SafeERC20 for IERC20; - bytes32 public constant CALLBACK_SUCCESS = keccak256("ERC3156FlashBorrower.onFlashLoan"); - uint32 public maxSwapSlippage; + bytes32 public constant CALLBACK_SUCCESS = keccak256("ERC3156FlashBorrower.onFlashLoan"); /// @notice Angle stablecoin flashloan contract - IERC3156FlashLender public immutable FLASHLOAN; - - /// @notice Reference to the `transmuter` implementation this contract aims at rebalancing - ITransmuter public immutable TRANSMUTER; - /// @notice AgToken handled by the `transmuter` of interest - address public immutable AGTOKEN; - /// @notice Max slippage when dealing with the Transmuter - uint96 public maxSlippage; - /// @notice Data associated to a collateral - mapping(address => CollatParams) public collateralData; + IERC3156FlashLender public immutable flashloan; + /// @notice Maximum slippage for swaps + uint32 public maxSwapSlippage; /// @notice Budget of AGToken available for each users mapping(address => uint256) public budget; @@ -66,36 +46,23 @@ contract GenericHarvester is AccessControl, IERC3156FlashBorrower, RouterSwapper //////////////////////////////////////////////////////////////////////////////////////////////////////////////////*/ constructor( - address transmuter, - address collateral, - address asset, - address flashloan, - uint64 targetExposure, - uint64 overrideExposures, - uint64 maxExposureYieldAsset, - uint64 minExposureYieldAsset, - uint96 _maxSlippage, - address _tokenTransferAddress, - address _swapRouter, - uint32 _maxSwapSlippage - ) RouterSwapper(_swapRouter, _tokenTransferAddress) { - if (flashloan == address(0)) revert ZeroAddress(); - FLASHLOAN = IERC3156FlashLender(flashloan); - TRANSMUTER = ITransmuter(transmuter); - AGTOKEN = address(ITransmuter(transmuter).agToken()); - - IERC20(AGTOKEN).safeApprove(flashloan, type(uint256).max); - accessControlManager = IAccessControlManager(ITransmuter(transmuter).accessControlManager()); - _setCollateralData( - collateral, - asset, - targetExposure, - minExposureYieldAsset, - maxExposureYieldAsset, - overrideExposures - ); - maxSwapSlippage = _maxSwapSlippage; - _setMaxSlippage(_maxSlippage); + uint96 initialMaxSlippage, + address initialTokenTransferAddress, + address initialSwapRouter, + uint32 initialMaxSwapSlippage, + IAgToken definitiveAgToken, + ITransmuter definitiveTransmuter, + IAccessControlManager definitiveAccessControlManager, + IERC3156FlashLender definitiveFlashloan + ) + RouterSwapper(initialSwapRouter, initialTokenTransferAddress) + BaseRebalancer(initialMaxSlippage, definitiveAccessControlManager, definitiveAgToken, definitiveTransmuter) + { + if (address(definitiveFlashloan) == address(0)) revert ZeroAddress(); + flashloan = definitiveFlashloan; + + IERC20(agToken).safeApprove(address(definitiveFlashloan), type(uint256).max); + maxSwapSlippage = initialMaxSwapSlippage; } /*////////////////////////////////////////////////////////////////////////////////////////////////////////////////// @@ -117,9 +84,9 @@ contract GenericHarvester is AccessControl, IERC3156FlashBorrower, RouterSwapper SwapType swapType, bytes calldata extraData ) public virtual { - FLASHLOAN.flashLoan( + flashloan.flashLoan( IERC3156FlashBorrower(address(this)), - address(AGTOKEN), + address(agToken), amountStablecoins, abi.encode(msg.sender, increase, collateral, asset, minAmountOut, swapType, extraData) ); @@ -133,7 +100,7 @@ contract GenericHarvester is AccessControl, IERC3156FlashBorrower, RouterSwapper uint256 fee, bytes calldata data ) public virtual returns (bytes32) { - if (msg.sender != address(FLASHLOAN) || initiator != address(this) || fee != 0) revert NotTrusted(); + if (msg.sender != address(flashloan) || initiator != address(this) || fee != 0) revert NotTrusted(); address sender; uint256 typeAction; uint256 minAmountOut; @@ -158,17 +125,24 @@ contract GenericHarvester is AccessControl, IERC3156FlashBorrower, RouterSwapper tokenOut = asset; } } - uint256 amountOut = TRANSMUTER.swapExactInput(amount, 0, AGTOKEN, tokenOut, address(this), block.timestamp); + uint256 amountOut = transmuter.swapExactInput( + amount, + 0, + address(agToken), + tokenOut, + address(this), + block.timestamp + ); // Swap to tokenIn amountOut = _swapToTokenIn(typeAction, tokenIn, tokenOut, amountOut, swapType, callData); - _adjustAllowance(tokenIn, address(TRANSMUTER), amountOut); - uint256 amountStableOut = TRANSMUTER.swapExactInput( + _adjustAllowance(tokenIn, address(transmuter), amountOut); + uint256 amountStableOut = transmuter.swapExactInput( amountOut, minAmountOut, tokenIn, - AGTOKEN, + address(agToken), address(this), block.timestamp ); @@ -186,7 +160,7 @@ contract GenericHarvester is AccessControl, IERC3156FlashBorrower, RouterSwapper * @param receiver address of the receiver */ function addBudget(uint256 amount, address receiver) public virtual { - IERC20(AGTOKEN).safeTransferFrom(msg.sender, address(this), amount); + IERC20(agToken).safeTransferFrom(msg.sender, address(this), amount); budget[receiver] += amount; } @@ -200,7 +174,7 @@ contract GenericHarvester is AccessControl, IERC3156FlashBorrower, RouterSwapper if (budget[receiver] < amount) revert InsufficientFunds(); budget[receiver] -= amount; - IERC20(AGTOKEN).safeTransfer(receiver, amount); + IERC20(agToken).safeTransfer(receiver, amount); } function _swapToTokenIn( @@ -210,11 +184,11 @@ contract GenericHarvester is AccessControl, IERC3156FlashBorrower, RouterSwapper uint256 amount, SwapType swapType, bytes memory callData - ) internal returns (uint256) { + ) internal returns (uint256 amountOut) { if (swapType == SwapType.SWAP) { - return _swapToTokenInSwap(tokenIn, tokenOut, amount, callData); + amountOut = _swapToTokenInSwap(tokenIn, tokenOut, amount, callData); } else if (swapType == SwapType.VAULT) { - return _swapToTokenInVault(typeAction, tokenIn, tokenOut, amount); + amountOut = _swapToTokenInVault(typeAction, tokenIn, tokenOut, amount); } } @@ -288,34 +262,12 @@ contract GenericHarvester is AccessControl, IERC3156FlashBorrower, RouterSwapper /// to the target exposure function harvest(address collateral, uint256 scale, SwapType swapType, bytes calldata extraData) public virtual { if (scale > 1e9) revert InvalidParam(); - (uint256 stablecoinsFromCollateral, uint256 stablecoinsIssued) = TRANSMUTER.getIssuedByCollateral(collateral); CollatParams memory collatInfo = collateralData[collateral]; - (uint256 stablecoinsFromAsset, ) = TRANSMUTER.getIssuedByCollateral(collatInfo.asset); - uint8 increase; - uint256 amount; - uint256 targetExposureScaled = collatInfo.targetExposure * stablecoinsIssued; - if (stablecoinsFromCollateral * 1e9 > targetExposureScaled) { - // Need to increase exposure to yield bearing asset - increase = 1; - amount = stablecoinsFromCollateral - targetExposureScaled / 1e9; - uint256 maxValueScaled = collatInfo.maxExposureYieldAsset * stablecoinsIssued; - // These checks assume that there are no transaction fees on the stablecoin->collateral conversion and so - // it's still possible that exposure goes above the max exposure in some rare cases - 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 (stablecoinsFromAsset * 1e9 < minValueScaled) amount = 0; - else if (stablecoinsFromAsset * 1e9 < minValueScaled + amount * 1e9) - amount = stablecoinsFromAsset - minValueScaled / 1e9; - } + (uint8 increase, uint256 amount) = _computeRebalanceAmount(collateral, collatInfo); amount = (amount * scale) / 1e9; + if (amount > 0) { - try TRANSMUTER.updateOracle(collatInfo.asset) {} catch {} + try transmuter.updateOracle(collatInfo.asset) {} catch {} adjustYieldExposure( amount, @@ -333,28 +285,6 @@ contract GenericHarvester is AccessControl, IERC3156FlashBorrower, RouterSwapper SETTERS //////////////////////////////////////////////////////////////////////////////////////////////////////////////////*/ - function setCollateralData( - address collateral, - address asset, - uint64 targetExposure, - uint64 minExposureYieldAsset, - uint64 maxExposureYieldAsset, - uint64 overrideExposures - ) public virtual onlyGuardian { - _setCollateralData( - collateral, - asset, - targetExposure, - minExposureYieldAsset, - maxExposureYieldAsset, - overrideExposures - ); - } - - function setMaxSlippage(uint96 _maxSlippage) public virtual onlyGuardian { - _setMaxSlippage(_maxSlippage); - } - /** * @notice Set the token transfer address * @param newTokenTransferAddress address of the token transfer contract @@ -373,65 +303,9 @@ contract GenericHarvester is AccessControl, IERC3156FlashBorrower, RouterSwapper /** * @notice Set the max swap slippage - * @param _maxSwapSlippage max slippage in BPS + * @param newMaxSwapSlippage max slippage in BPS */ - function setMaxSwapSlippage(uint32 _maxSwapSlippage) external onlyGuardian { - maxSwapSlippage = _maxSwapSlippage; - } - - function updateLimitExposuresYieldAsset(address collateral) public virtual onlyGuardian { - CollatParams storage collatInfo = collateralData[collateral]; - if (collatInfo.overrideExposures == 2) _updateLimitExposuresYieldAsset(collatInfo); - } - - function _setMaxSlippage(uint96 _maxSlippage) internal virtual { - if (_maxSlippage > 1e9) revert InvalidParam(); - maxSlippage = _maxSlippage; - } - - function _setCollateralData( - address collateral, - address asset, - uint64 targetExposure, - uint64 minExposureYieldAsset, - uint64 maxExposureYieldAsset, - uint64 overrideExposures - ) internal virtual { - CollatParams storage collatInfo = collateralData[collateral]; - collatInfo.asset = asset; - if (targetExposure >= 1e9) revert InvalidParam(); - collatInfo.targetExposure = targetExposure; - collatInfo.overrideExposures = overrideExposures; - if (overrideExposures == 1) { - if (maxExposureYieldAsset >= 1e9 || minExposureYieldAsset >= maxExposureYieldAsset) revert InvalidParam(); - collatInfo.maxExposureYieldAsset = maxExposureYieldAsset; - collatInfo.minExposureYieldAsset = minExposureYieldAsset; - } else { - collatInfo.overrideExposures = 2; - _updateLimitExposuresYieldAsset(collatInfo); - } - } - - function _updateLimitExposuresYieldAsset(CollatParams storage collatInfo) internal virtual { - uint64[] memory xFeeMint; - (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.asset); - length = xFeeBurn.length; - if (length <= 1) collatInfo.minExposureYieldAsset = 0; - else collatInfo.minExposureYieldAsset = xFeeBurn[length - 2]; - } - - /*////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - 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); + function setMaxSwapSlippage(uint32 newMaxSwapSlippage) external onlyGuardian { + maxSwapSlippage = newMaxSwapSlippage; } } diff --git a/contracts/helpers/MultiBlockRebalancer.sol b/contracts/helpers/MultiBlockRebalancer.sol index d4e3ea2c..ffcbdac0 100644 --- a/contracts/helpers/MultiBlockRebalancer.sol +++ b/contracts/helpers/MultiBlockRebalancer.sol @@ -2,29 +2,18 @@ pragma solidity ^0.8.23; +import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import { AccessControl, IAccessControlManager } from "../utils/AccessControl.sol"; +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import { BaseRebalancer, CollatParams } from "./BaseRebalancer.sol"; +import { ITransmuter } from "../interfaces/ITransmuter.sol"; import { IAgToken } from "../interfaces/IAgToken.sol"; import { IPool } from "../interfaces/IPool.sol"; -import { ITransmuter } from "../interfaces/ITransmuter.sol"; -import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; -import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import "../utils/Errors.sol"; import "../utils/Constants.sol"; -struct CollatParams { - // Target exposure to the collateral asset used - uint64 targetExposure; - // Maximum exposure within the Transmuter to the asset - uint64 maxExposureYieldAsset; - // 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; -} - -contract MultiBlockRebalancer is AccessControl { +contract MultiBlockRebalancer is BaseRebalancer { using SafeERC20 for IERC20; /*////////////////////////////////////////////////////////////////////////////////////////////////////////////////// @@ -42,17 +31,11 @@ contract MultiBlockRebalancer is AccessControl { /// @notice address to deposit to receive collateral mapping(address => address) public collateralToDepositAddress; - /// @notice Data associated to a collateral - mapping(address => CollatParams) public collateralData; /// @notice trusted addresses mapping(address => bool) public isTrusted; /// @notice Maximum amount of stablecoins that can be minted in a single transaction uint256 public maxMintAmount; - /// @notice Max slippage when dealing with the Transmuter - uint96 public maxSlippage; - ITransmuter public transmuter; - IAgToken public agToken; /*////////////////////////////////////////////////////////////////////////////////////////////////////////////////// CONSTRUCTOR @@ -63,13 +46,9 @@ contract MultiBlockRebalancer is AccessControl { uint96 initialMaxSlippage, IAccessControlManager definitiveAccessControlManager, IAgToken definitiveAgToken, - ITransmuter definitivetransmuter - ) { - _setMaxSlippage(initialMaxSlippage); + ITransmuter definitiveTransmuter + ) BaseRebalancer(initialMaxSlippage, definitiveAccessControlManager, definitiveAgToken, definitiveTransmuter) { maxMintAmount = initialMaxMintAmount; - accessControlManager = definitiveAccessControlManager; - agToken = definitiveAgToken; - transmuter = definitivetransmuter; } /*////////////////////////////////////////////////////////////////////////////////////////////////////////////////// @@ -88,44 +67,6 @@ contract MultiBlockRebalancer is AccessControl { GUARDIAN FUNCTIONS //////////////////////////////////////////////////////////////////////////////////////////////////////////////////*/ - /** - * @notice Set the collateral data - * @param collateral address of the collateral - * @param targetExposure target exposure to the collateral asset used - * @param minExposureYieldAsset minimum exposure within the Transmuter to the asset - * @param maxExposureYieldAsset maximum exposure within the Transmuter to the asset - * @param overrideExposures 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 - */ - function setCollateralData( - address collateral, - uint64 targetExposure, - uint64 minExposureYieldAsset, - uint64 maxExposureYieldAsset, - uint64 overrideExposures - ) external onlyGuardian { - CollatParams storage collatInfo = collateralData[collateral]; - if (targetExposure >= 1e9) revert InvalidParam(); - collatInfo.targetExposure = targetExposure; - collatInfo.overrideExposures = overrideExposures; - if (overrideExposures == 1) { - if (maxExposureYieldAsset >= 1e9 || minExposureYieldAsset >= maxExposureYieldAsset) revert InvalidParam(); - collatInfo.maxExposureYieldAsset = maxExposureYieldAsset; - collatInfo.minExposureYieldAsset = minExposureYieldAsset; - } else { - collatInfo.overrideExposures = 2; - _updateLimitExposuresYieldAsset(collateral, collatInfo); - } - } - - /** - * @notice Set the max allowed slippage - * @param newMaxSlippage new max allowed slippage - */ - function setMaxSlippage(uint96 newMaxSlippage) external onlyGuardian { - _setMaxSlippage(newMaxSlippage); - } - /** * @notice Set the deposit address for a collateral * @param collateral address of the collateral @@ -146,7 +87,8 @@ contract MultiBlockRebalancer is AccessControl { */ function initiateRebalance(uint256 scale, address collateral) external onlyTrusted { if (scale > 1e9) revert InvalidParam(); - (uint8 increase, uint256 amount) = _computeRebalanceAmount(collateral); + CollatParams memory collatInfo = collateralData[collateral]; + (uint8 increase, uint256 amount) = _computeRebalanceAmount(collateral, collatInfo); amount = (amount * scale) / 1e9; try transmuter.updateOracle(collateral) {} catch {} @@ -179,32 +121,6 @@ contract MultiBlockRebalancer is AccessControl { INTERNAL FUNCTIONS //////////////////////////////////////////////////////////////////////////////////////////////////////////////////*/ - function _computeRebalanceAmount(address collateral) internal view returns (uint8 increase, uint256 amount) { - (uint256 stablecoinsFromCollateral, uint256 stablecoinsIssued) = transmuter.getIssuedByCollateral(collateral); - CollatParams memory collatInfo = collateralData[collateral]; - (uint256 stablecoinsFromAsset, ) = transmuter.getIssuedByCollateral(collateral); - uint256 targetExposureScaled = collatInfo.targetExposure * stablecoinsIssued; - if (stablecoinsFromCollateral * 1e9 > targetExposureScaled) { - // Need to increase exposure to yield bearing asset - increase = 1; - amount = stablecoinsFromCollateral - targetExposureScaled / 1e9; - uint256 maxValueScaled = collatInfo.maxExposureYieldAsset * stablecoinsIssued; - // These checks assume that there are no transaction fees on the stablecoin->collateral conversion and so - // it's still possible that exposure goes above the max exposure in some rare cases - 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 (stablecoinsFromAsset * 1e9 < minValueScaled) amount = 0; - else if (stablecoinsFromAsset * 1e9 < minValueScaled + amount * 1e9) - amount = stablecoinsFromAsset - minValueScaled / 1e9; - } - } - function _rebalance(uint8 typeAction, address collateral, uint256 amount) internal { if (amount > maxMintAmount) revert TooBigAmountIn(); agToken.mint(address(this), amount); @@ -244,7 +160,7 @@ contract MultiBlockRebalancer is AccessControl { block.timestamp ); _checkSlippage(amount, amountOut, collateral, depositAddress); - IERC20(collateral).transfer(depositAddress, amountOut); + IERC20(collateral).safeTransfer(depositAddress, amountOut); } } else { uint256 amountOut = transmuter.swapExactInput( @@ -261,39 +177,15 @@ contract MultiBlockRebalancer is AccessControl { if (collateral == XEVT) { IPool(depositAddress).requestRedeem(amountOut); } else if (collateral == USDM) { - IERC20(collateral).transfer(depositAddress, amountOut); + IERC20(collateral).safeTransfer(depositAddress, amountOut); } } } - function _updateLimitExposuresYieldAsset(address collateral, CollatParams storage collatInfo) internal virtual { - uint64[] memory xFeeMint; - (xFeeMint, ) = transmuter.getCollateralMintFees(collateral); - uint256 length = xFeeMint.length; - if (length <= 1) collatInfo.maxExposureYieldAsset = 1e9; - else collatInfo.maxExposureYieldAsset = xFeeMint[length - 2]; - - uint64[] memory xFeeBurn; - (xFeeBurn, ) = transmuter.getCollateralBurnFees(collateral); - length = xFeeBurn.length; - if (length <= 1) collatInfo.minExposureYieldAsset = 0; - else collatInfo.minExposureYieldAsset = xFeeBurn[length - 2]; - } - - function _setMaxSlippage(uint96 newMaxSlippage) internal virtual { - if (newMaxSlippage > 1e9) revert InvalidParam(); - maxSlippage = newMaxSlippage; - } - /*////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 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); - } - function _checkSlippage( uint256 amountIn, uint256 amountOut, diff --git a/test/utils/FunctionUtils.sol b/test/utils/FunctionUtils.sol index ad48550e..ccf1d6b4 100644 --- a/test/utils/FunctionUtils.sol +++ b/test/utils/FunctionUtils.sol @@ -21,7 +21,7 @@ contract FunctionUtils is StdUtils { bool mint, int256 minFee, int256 maxFee - ) internal view returns (uint64[] memory postThres, int64[] memory postIntercep) { + ) internal pure returns (uint64[] memory postThres, int64[] memory postIntercep) { thresholds[0] = increasing ? 0 : uint64(BASE_9); intercepts[0] = int64(bound(int256(intercepts[0]), minFee, maxFee)); uint256 nbrInflexion = 1; From b7e1a89ffd00203cdf26f121e0689f70ae54246c Mon Sep 17 00:00:00 2001 From: 0xtekgrinder <0xtekgrinder@protonmail.com> Date: Fri, 20 Sep 2024 16:02:19 +0200 Subject: [PATCH 11/69] feat: deploy scripts for new harvesters --- scripts/AdjustYieldExposure.s.sol | 14 +++++-- scripts/DeployGenericHarvester.s.sol | 42 +++++++++++++++++++ scripts/DeployHarvesterSwap.s.sol | 36 ---------------- scripts/DeployHarvesterVault.s.sol | 34 --------------- scripts/DeployMultiBlockRebalancer.s.sol | 36 ++++++++++++++++ scripts/DeployRebalancer.s.sol | 28 ------------- scripts/DeployRebalancerFlashloanSwap.s.sol | 36 ---------------- scripts/DeployRebalancerFlashloanVault.s.sol | 39 ----------------- .../test/UpdateTransmuterFacetsUSDATest.s.sol | 4 -- 9 files changed, 89 insertions(+), 180 deletions(-) create mode 100644 scripts/DeployGenericHarvester.s.sol delete mode 100644 scripts/DeployHarvesterSwap.s.sol delete mode 100644 scripts/DeployHarvesterVault.s.sol create mode 100644 scripts/DeployMultiBlockRebalancer.s.sol delete mode 100644 scripts/DeployRebalancer.s.sol delete mode 100644 scripts/DeployRebalancerFlashloanSwap.s.sol delete mode 100644 scripts/DeployRebalancerFlashloanVault.s.sol diff --git a/scripts/AdjustYieldExposure.s.sol b/scripts/AdjustYieldExposure.s.sol index b3c9e55d..06682927 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 { RebalancerFlashloanVault } from "contracts/helpers/RebalancerFlashloanVault.sol"; +import { GenericHarvester, SwapType } from "contracts/helpers/GenericHarvester.sol"; contract AdjustYieldExposure is Utils { function run() external { @@ -17,8 +17,16 @@ contract AdjustYieldExposure is Utils { console.log(deployer.balance); vm.startBroadcast(deployerPrivateKey); - RebalancerFlashloanVault rebalancer = RebalancerFlashloanVault(0x22604C0E5633A9810E01c9cb469B23Eee17AC411); - rebalancer.adjustYieldExposure(1300000 * 1 ether, 0, USDC, STEAK_USDC, 1200000 * 1 ether, new bytes(0)); + GenericHarvester rebalancer = GenericHarvester(0x22604C0E5633A9810E01c9cb469B23Eee17AC411); + rebalancer.adjustYieldExposure( + 1300000 * 1 ether, + 0, + USDC, + STEAK_USDC, + 1200000 * 1 ether, + SwapType.VAULT, + new bytes(0) + ); vm.stopBroadcast(); } diff --git a/scripts/DeployGenericHarvester.s.sol b/scripts/DeployGenericHarvester.s.sol new file mode 100644 index 00000000..a2561559 --- /dev/null +++ b/scripts/DeployGenericHarvester.s.sol @@ -0,0 +1,42 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity ^0.8.19; + +import "./utils/Utils.s.sol"; +import { console } from "forge-std/console.sol"; +import { GenericHarvester } from "contracts/helpers/GenericHarvester.sol"; +import { IAccessControlManager } from "contracts/utils/AccessControl.sol"; +import { IAgToken } from "contracts/interfaces/IAgToken.sol"; +import { ITransmuter } from "contracts/interfaces/ITransmuter.sol"; +import { IERC3156FlashLender } from "oz/interfaces/IERC3156FlashLender.sol"; +import "./Constants.s.sol"; + +contract DeployGenericHarvester 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); + uint256 maxMintAmount = 1000000e18; + uint96 maxSlippage = 1e9 / 100; + uint32 maxSwapSlippage = 100; // 1% + IERC3156FlashLender flashloan = IERC3156FlashLender(_chainToContract(CHAIN_SOURCE, ContractType.FlashLoan)); + IAgToken agToken = IAgToken(_chainToContract(CHAIN_SOURCE, ContractType.AgEUR)); + ITransmuter transmuter = ITransmuter(_chainToContract(CHAIN_SOURCE, ContractType.TransmuterAgEUR)); + IAccessControlManager accessControlManager = transmuter.accessControlManager(); + + GenericHarvester harvester = new GenericHarvester( + maxSlippage, + ONEINCH_ROUTER, + ONEINCH_ROUTER, + maxSwapSlippage, + agToken, + transmuter, + accessControlManager, + flashloan + ); + console.log("HarvesterVault deployed at: ", address(harvester)); + + vm.stopBroadcast(); + } +} diff --git a/scripts/DeployHarvesterSwap.s.sol b/scripts/DeployHarvesterSwap.s.sol deleted file mode 100644 index a6817db3..00000000 --- a/scripts/DeployHarvesterSwap.s.sol +++ /dev/null @@ -1,36 +0,0 @@ -// 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/DeployHarvesterVault.s.sol b/scripts/DeployHarvesterVault.s.sol deleted file mode 100644 index 085e567f..00000000 --- a/scripts/DeployHarvesterVault.s.sol +++ /dev/null @@ -1,34 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0 -pragma solidity ^0.8.19; - -import "./utils/Utils.s.sol"; -import { console } from "forge-std/console.sol"; -import { HarvesterVault } from "contracts/helpers/HarvesterVault.sol"; -import "./Constants.s.sol"; - -contract DeployHarvesterVault 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 vault = STEAK_USDC; - uint64 targetExposure = (13 * 1e9) / 100; - uint64 overrideExposures = 0; - uint96 maxSlippage = 1e9 / 100; - HarvesterVault harvester = new HarvesterVault( - rebalancer, - vault, - targetExposure, - overrideExposures, - 0, - 0, - maxSlippage - ); - console.log("HarvesterVault deployed at: ", address(harvester)); - - vm.stopBroadcast(); - } -} diff --git a/scripts/DeployMultiBlockRebalancer.s.sol b/scripts/DeployMultiBlockRebalancer.s.sol new file mode 100644 index 00000000..47a6890d --- /dev/null +++ b/scripts/DeployMultiBlockRebalancer.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 { MultiBlockRebalancer } from "contracts/helpers/MultiBlockRebalancer.sol"; +import { IAccessControlManager } from "contracts/utils/AccessControl.sol"; +import { IAgToken } from "contracts/interfaces/IAgToken.sol"; +import { ITransmuter } from "contracts/interfaces/ITransmuter.sol"; +import "./Constants.s.sol"; + +contract DeployMultiBlockRebalancer 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); + uint256 maxMintAmount = 1000000e18; + uint96 maxSlippage = 1e9 / 100; + address agToken = _chainToContract(CHAIN_SOURCE, ContractType.AgEUR); + address transmuter = _chainToContract(CHAIN_SOURCE, ContractType.TransmuterAgEUR); + IAccessControlManager accessControlManager = ITransmuter(transmuter).accessControlManager(); + + MultiBlockRebalancer harvester = new MultiBlockRebalancer( + maxMintAmount, + maxSlippage, + accessControlManager, + IAgToken(agToken), + ITransmuter(transmuter) + ); + console.log("HarvesterVault deployed at: ", address(harvester)); + + vm.stopBroadcast(); + } +} diff --git a/scripts/DeployRebalancer.s.sol b/scripts/DeployRebalancer.s.sol deleted file mode 100644 index 63da1c35..00000000 --- a/scripts/DeployRebalancer.s.sol +++ /dev/null @@ -1,28 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0 -pragma solidity ^0.8.19; - -import "./utils/Utils.s.sol"; -import { console } from "forge-std/console.sol"; -import { Rebalancer } from "contracts/helpers/Rebalancer.sol"; -import { IAccessControlManager } from "contracts/utils/AccessControl.sol"; -import { ITransmuter } from "contracts/interfaces/ITransmuter.sol"; -import "./Constants.s.sol"; -import "oz/interfaces/IERC20.sol"; -import "oz-upgradeable/token/ERC20/extensions/IERC20MetadataUpgradeable.sol"; - -contract DeployRebalancer is Utils { - function run() external { - uint256 deployerPrivateKey = vm.deriveKey(vm.envString("MNEMONIC_FORK"), "m/44'/60'/0'/0/", 0); - vm.startBroadcast(deployerPrivateKey); - - address deployer = vm.addr(deployerPrivateKey); - console.log("Deployer address: ", deployer); - Rebalancer rebalancer = new Rebalancer( - IAccessControlManager(_chainToContract(CHAIN_SOURCE, ContractType.CoreBorrow)), - ITransmuter(_chainToContract(CHAIN_SOURCE, ContractType.TransmuterAgEUR)) - ); - console.log("Rebalancer deployed at: ", address(rebalancer)); - - vm.stopBroadcast(); - } -} diff --git a/scripts/DeployRebalancerFlashloanSwap.s.sol b/scripts/DeployRebalancerFlashloanSwap.s.sol deleted file mode 100644 index 977cc7a3..00000000 --- a/scripts/DeployRebalancerFlashloanSwap.s.sol +++ /dev/null @@ -1,36 +0,0 @@ -// 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/DeployRebalancerFlashloanVault.s.sol b/scripts/DeployRebalancerFlashloanVault.s.sol deleted file mode 100644 index 1f1df245..00000000 --- a/scripts/DeployRebalancerFlashloanVault.s.sol +++ /dev/null @@ -1,39 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0 -pragma solidity ^0.8.19; - -import "./utils/Utils.s.sol"; -import { console } from "forge-std/console.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"; -import "./Constants.s.sol"; -import "oz/interfaces/IERC20.sol"; -import "oz-upgradeable/token/ERC20/extensions/IERC20MetadataUpgradeable.sol"; - -contract DeployRebalancerFlashloanVault 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)))); - RebalancerFlashloanVault rebalancer = new RebalancerFlashloanVault( - IAccessControlManager(_chainToContract(CHAIN_SOURCE, ContractType.CoreBorrow)), - ITransmuter(_chainToContract(CHAIN_SOURCE, ContractType.TransmuterAgUSD)), - IERC3156FlashLender(0x4A2FF9bC686A0A23DA13B6194C69939189506F7F) - ); - /* - RebalancerFlashloanVault rebalancer = new RebalancerFlashloanVault( - IAccessControlManager(0x5bc6BEf80DA563EBf6Df6D6913513fa9A7ec89BE), - ITransmuter(0x222222fD79264BBE280b4986F6FEfBC3524d0137), - IERC3156FlashLender(0x4A2FF9bC686A0A23DA13B6194C69939189506F7F) - ); - */ - console.log("Rebalancer deployed at: ", address(rebalancer)); - - vm.stopBroadcast(); - } -} diff --git a/scripts/test/UpdateTransmuterFacetsUSDATest.s.sol b/scripts/test/UpdateTransmuterFacetsUSDATest.s.sol index ef360bef..5b45436d 100644 --- a/scripts/test/UpdateTransmuterFacetsUSDATest.s.sol +++ b/scripts/test/UpdateTransmuterFacetsUSDATest.s.sol @@ -32,8 +32,6 @@ interface OldTransmuter { contract UpdateTransmuterFacetsUSDATest is Helpers, Test { using stdJson for string; - uint256 public CHAIN_SOURCE; - address constant WHALE_USDA = 0xA9DdD91249DFdd450E81E1c56Ab60E1A62651701; string[] replaceFacetNames; @@ -51,8 +49,6 @@ contract UpdateTransmuterFacetsUSDATest is Helpers, Test { function setUp() public override { super.setUp(); - CHAIN_SOURCE = CHAIN_ETHEREUM; - ethereumFork = vm.createSelectFork("mainnet", 19499622); governor = DEPLOYER; From 9afc2145b8c1146be445700fcd3448ec66a77abf Mon Sep 17 00:00:00 2001 From: 0xtekgrinder <0xtekgrinder@protonmail.com> Date: Fri, 20 Sep 2024 16:15:46 +0200 Subject: [PATCH 12/69] style: format BaseRebalancer --- contracts/helpers/BaseRebalancer.sol | 1 + 1 file changed, 1 insertion(+) diff --git a/contracts/helpers/BaseRebalancer.sol b/contracts/helpers/BaseRebalancer.sol index ec6a39ca..4e23e0e6 100644 --- a/contracts/helpers/BaseRebalancer.sol +++ b/contracts/helpers/BaseRebalancer.sol @@ -55,6 +55,7 @@ abstract contract BaseRebalancer is AccessControl { agToken = definitiveAgToken; transmuter = definitiveTransmuter; } + /*////////////////////////////////////////////////////////////////////////////////////////////////////////////////// GUARDIAN FUNCTIONS //////////////////////////////////////////////////////////////////////////////////////////////////////////////////*/ From 6580fddc688799f4d8300b16a2d89d9bf813871d Mon Sep 17 00:00:00 2001 From: 0xtekgrinder <0xtekgrinder@protonmail.com> Date: Fri, 20 Sep 2024 16:44:59 +0200 Subject: [PATCH 13/69] feat: remove comment --- contracts/helpers/GenericHarvester.sol | 1 - 1 file changed, 1 deletion(-) diff --git a/contracts/helpers/GenericHarvester.sol b/contracts/helpers/GenericHarvester.sol index 3a0293d1..4fc347cd 100644 --- a/contracts/helpers/GenericHarvester.sol +++ b/contracts/helpers/GenericHarvester.sol @@ -147,7 +147,6 @@ contract GenericHarvester is BaseRebalancer, IERC3156FlashBorrower, RouterSwappe block.timestamp ); if (amount > amountStableOut) { - // TODO temporary fix for for subsidy as stack too deep if (budget[sender] < amount - amountStableOut) revert InsufficientFunds(); budget[sender] -= amount - amountStableOut; } From c2c6d061e3b2246de26a8b46c8d61604d1776041 Mon Sep 17 00:00:00 2001 From: 0xtekgrinder <0xtekgrinder@protonmail.com> Date: Fri, 20 Sep 2024 16:45:09 +0200 Subject: [PATCH 14/69] chore: remove useless file --- wow | 769 ------------------------------------------------------------ 1 file changed, 769 deletions(-) delete mode 100644 wow diff --git a/wow b/wow deleted file mode 100644 index 090b6730..00000000 --- a/wow +++ /dev/null @@ -1,769 +0,0 @@ -Compiling 8 files with Solc 0.8.23 -Solc 0.8.23 finished in 143.42ms -Compiler run successful! -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.0 ^0.8.1 ^0.8.2; - -// lib/openzeppelin-contracts/contracts/interfaces/draft-IERC1822.sol - -// OpenZeppelin Contracts (last updated v4.5.0) (interfaces/draft-IERC1822.sol) - -/** - * @dev ERC1822: Universal Upgradeable Proxy Standard (UUPS) documents a method for upgradeability through a simplified - * proxy whose upgrades are fully controlled by the current implementation. - */ -interface IERC1822Proxiable { - /** - * @dev Returns the storage slot that the proxiable contract assumes is being used to store the implementation - * address. - * - * IMPORTANT: A proxy pointing at a proxiable contract should not be considered proxiable itself, because this risks - * bricking a proxy that upgrades to it, by delegating to itself until out of gas. Thus it is critical that this - * function revert if invoked through a proxy. - */ - function proxiableUUID() external view returns (bytes32); -} - -// lib/openzeppelin-contracts/contracts/proxy/Proxy.sol - -// OpenZeppelin Contracts (last updated v4.6.0) (proxy/Proxy.sol) - -/** - * @dev This abstract contract provides a fallback function that delegates all calls to another contract using the EVM - * instruction `delegatecall`. We refer to the second contract as the _implementation_ behind the proxy, and it has to - * be specified by overriding the virtual {_implementation} function. - * - * Additionally, delegation to the implementation can be triggered manually through the {_fallback} function, or to a - * different contract through the {_delegate} function. - * - * The success and return data of the delegated call will be returned back to the caller of the proxy. - */ -abstract contract Proxy { - /** - * @dev Delegates the current call to `implementation`. - * - * This function does not return to its internal call site, it will return directly to the external caller. - */ - function _delegate(address implementation) internal virtual { - assembly { - // Copy msg.data. We take full control of memory in this inline assembly - // block because it will not return to Solidity code. We overwrite the - // Solidity scratch pad at memory position 0. - calldatacopy(0, 0, calldatasize()) - - // Call the implementation. - // out and outsize are 0 because we don't know the size yet. - let result := delegatecall(gas(), implementation, 0, calldatasize(), 0, 0) - - // Copy the returned data. - returndatacopy(0, 0, returndatasize()) - - switch result - // delegatecall returns 0 on error. - case 0 { - revert(0, returndatasize()) - } - default { - return(0, returndatasize()) - } - } - } - - /** - * @dev This is a virtual function that should be overridden so it returns the address to which the fallback function - * and {_fallback} should delegate. - */ - function _implementation() internal view virtual returns (address); - - /** - * @dev Delegates the current call to the address returned by `_implementation()`. - * - * This function does not return to its internal call site, it will return directly to the external caller. - */ - function _fallback() internal virtual { - _beforeFallback(); - _delegate(_implementation()); - } - - /** - * @dev Fallback function that delegates calls to the address returned by `_implementation()`. Will run if no other - * function in the contract matches the call data. - */ - fallback() external payable virtual { - _fallback(); - } - - /** - * @dev Fallback function that delegates calls to the address returned by `_implementation()`. Will run if call data - * is empty. - */ - receive() external payable virtual { - _fallback(); - } - - /** - * @dev Hook that is called before falling back to the implementation. Can happen as part of a manual `_fallback` - * call, or as part of the Solidity `fallback` or `receive` functions. - * - * If overridden should call `super._beforeFallback()`. - */ - function _beforeFallback() internal virtual {} -} - -// lib/openzeppelin-contracts/contracts/proxy/beacon/IBeacon.sol - -// OpenZeppelin Contracts v4.4.1 (proxy/beacon/IBeacon.sol) - -/** - * @dev This is the interface that {BeaconProxy} expects of its beacon. - */ -interface IBeacon { - /** - * @dev Must return an address that can be used as a delegate call target. - * - * {BeaconProxy} will check that this address is a contract. - */ - function implementation() external view returns (address); -} - -// lib/openzeppelin-contracts/contracts/utils/Address.sol - -// OpenZeppelin Contracts (last updated v4.7.0) (utils/Address.sol) - -/** - * @dev Collection of functions related to the address type - */ -library Address { - /** - * @dev Returns true if `account` is a contract. - * - * [IMPORTANT] - * ==== - * It is unsafe to assume that an address for which this function returns - * false is an externally-owned account (EOA) and not a contract. - * - * Among others, `isContract` will return false for the following - * types of addresses: - * - * - an externally-owned account - * - a contract in construction - * - an address where a contract will be created - * - an address where a contract lived, but was destroyed - * ==== - * - * [IMPORTANT] - * ==== - * You shouldn't rely on `isContract` to protect against flash loan attacks! - * - * Preventing calls from contracts is highly discouraged. It breaks composability, breaks support for smart wallets - * like Gnosis Safe, and does not provide security since it can be circumvented by calling from a contract - * constructor. - * ==== - */ - function isContract(address account) internal view returns (bool) { - // This method relies on extcodesize/address.code.length, which returns 0 - // for contracts in construction, since the code is only stored at the end - // of the constructor execution. - - return account.code.length > 0; - } - - /** - * @dev Replacement for Solidity's `transfer`: sends `amount` wei to - * `recipient`, forwarding all available gas and reverting on errors. - * - * https://eips.ethereum.org/EIPS/eip-1884[EIP1884] increases the gas cost - * of certain opcodes, possibly making contracts go over the 2300 gas limit - * imposed by `transfer`, making them unable to receive funds via - * `transfer`. {sendValue} removes this limitation. - * - * https://diligence.consensys.net/posts/2019/09/stop-using-soliditys-transfer-now/[Learn more]. - * - * IMPORTANT: because control is transferred to `recipient`, care must be - * taken to not create reentrancy vulnerabilities. Consider using - * {ReentrancyGuard} or the - * https://solidity.readthedocs.io/en/v0.5.11/security-considerations.html#use-the-checks-effects-interactions-pattern[checks-effects-interactions pattern]. - */ - function sendValue(address payable recipient, uint256 amount) internal { - require(address(this).balance >= amount, "Address: insufficient balance"); - - (bool success, ) = recipient.call{value: amount}(""); - require(success, "Address: unable to send value, recipient may have reverted"); - } - - /** - * @dev Performs a Solidity function call using a low level `call`. A - * plain `call` is an unsafe replacement for a function call: use this - * function instead. - * - * If `target` reverts with a revert reason, it is bubbled up by this - * function (like regular Solidity function calls). - * - * Returns the raw returned data. To convert to the expected return value, - * use https://solidity.readthedocs.io/en/latest/units-and-global-variables.html?highlight=abi.decode#abi-encoding-and-decoding-functions[`abi.decode`]. - * - * Requirements: - * - * - `target` must be a contract. - * - calling `target` with `data` must not revert. - * - * _Available since v3.1._ - */ - function functionCall(address target, bytes memory data) internal returns (bytes memory) { - return functionCall(target, data, "Address: low-level call failed"); - } - - /** - * @dev Same as {xref-Address-functionCall-address-bytes-}[`functionCall`], but with - * `errorMessage` as a fallback revert reason when `target` reverts. - * - * _Available since v3.1._ - */ - function functionCall( - address target, - bytes memory data, - string memory errorMessage - ) internal returns (bytes memory) { - return functionCallWithValue(target, data, 0, errorMessage); - } - - /** - * @dev Same as {xref-Address-functionCall-address-bytes-}[`functionCall`], - * but also transferring `value` wei to `target`. - * - * Requirements: - * - * - the calling contract must have an ETH balance of at least `value`. - * - the called Solidity function must be `payable`. - * - * _Available since v3.1._ - */ - function functionCallWithValue( - address target, - bytes memory data, - uint256 value - ) internal returns (bytes memory) { - return functionCallWithValue(target, data, value, "Address: low-level call with value failed"); - } - - /** - * @dev Same as {xref-Address-functionCallWithValue-address-bytes-uint256-}[`functionCallWithValue`], but - * with `errorMessage` as a fallback revert reason when `target` reverts. - * - * _Available since v3.1._ - */ - function functionCallWithValue( - address target, - bytes memory data, - uint256 value, - string memory errorMessage - ) internal returns (bytes memory) { - require(address(this).balance >= value, "Address: insufficient balance for call"); - require(isContract(target), "Address: call to non-contract"); - - (bool success, bytes memory returndata) = target.call{value: value}(data); - return verifyCallResult(success, returndata, errorMessage); - } - - /** - * @dev Same as {xref-Address-functionCall-address-bytes-}[`functionCall`], - * but performing a static call. - * - * _Available since v3.3._ - */ - function functionStaticCall(address target, bytes memory data) internal view returns (bytes memory) { - return functionStaticCall(target, data, "Address: low-level static call failed"); - } - - /** - * @dev Same as {xref-Address-functionCall-address-bytes-string-}[`functionCall`], - * but performing a static call. - * - * _Available since v3.3._ - */ - function functionStaticCall( - address target, - bytes memory data, - string memory errorMessage - ) internal view returns (bytes memory) { - require(isContract(target), "Address: static call to non-contract"); - - (bool success, bytes memory returndata) = target.staticcall(data); - return verifyCallResult(success, returndata, errorMessage); - } - - /** - * @dev Same as {xref-Address-functionCall-address-bytes-}[`functionCall`], - * but performing a delegate call. - * - * _Available since v3.4._ - */ - function functionDelegateCall(address target, bytes memory data) internal returns (bytes memory) { - return functionDelegateCall(target, data, "Address: low-level delegate call failed"); - } - - /** - * @dev Same as {xref-Address-functionCall-address-bytes-string-}[`functionCall`], - * but performing a delegate call. - * - * _Available since v3.4._ - */ - function functionDelegateCall( - address target, - bytes memory data, - string memory errorMessage - ) internal returns (bytes memory) { - require(isContract(target), "Address: delegate call to non-contract"); - - (bool success, bytes memory returndata) = target.delegatecall(data); - return verifyCallResult(success, returndata, errorMessage); - } - - /** - * @dev Tool to verifies that a low level call was successful, and revert if it wasn't, either by bubbling the - * revert reason using the provided one. - * - * _Available since v4.3._ - */ - function verifyCallResult( - bool success, - bytes memory returndata, - string memory errorMessage - ) internal pure returns (bytes memory) { - if (success) { - return returndata; - } else { - // Look for revert reason and bubble it up if present - if (returndata.length > 0) { - // The easiest way to bubble the revert reason is using memory via assembly - /// @solidity memory-safe-assembly - assembly { - let returndata_size := mload(returndata) - revert(add(32, returndata), returndata_size) - } - } else { - revert(errorMessage); - } - } - } -} - -// lib/openzeppelin-contracts/contracts/utils/StorageSlot.sol - -// OpenZeppelin Contracts (last updated v4.7.0) (utils/StorageSlot.sol) - -/** - * @dev Library for reading and writing primitive types to specific storage slots. - * - * Storage slots are often used to avoid storage conflict when dealing with upgradeable contracts. - * This library helps with reading and writing to such slots without the need for inline assembly. - * - * The functions in this library return Slot structs that contain a `value` member that can be used to read or write. - * - * Example usage to set ERC1967 implementation slot: - * ``` - * contract ERC1967 { - * bytes32 internal constant _IMPLEMENTATION_SLOT = 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc; - * - * function _getImplementation() internal view returns (address) { - * return StorageSlot.getAddressSlot(_IMPLEMENTATION_SLOT).value; - * } - * - * function _setImplementation(address newImplementation) internal { - * require(Address.isContract(newImplementation), "ERC1967: new implementation is not a contract"); - * StorageSlot.getAddressSlot(_IMPLEMENTATION_SLOT).value = newImplementation; - * } - * } - * ``` - * - * _Available since v4.1 for `address`, `bool`, `bytes32`, and `uint256`._ - */ -library StorageSlot { - struct AddressSlot { - address value; - } - - struct BooleanSlot { - bool value; - } - - struct Bytes32Slot { - bytes32 value; - } - - struct Uint256Slot { - uint256 value; - } - - /** - * @dev Returns an `AddressSlot` with member `value` located at `slot`. - */ - function getAddressSlot(bytes32 slot) internal pure returns (AddressSlot storage r) { - /// @solidity memory-safe-assembly - assembly { - r.slot := slot - } - } - - /** - * @dev Returns an `BooleanSlot` with member `value` located at `slot`. - */ - function getBooleanSlot(bytes32 slot) internal pure returns (BooleanSlot storage r) { - /// @solidity memory-safe-assembly - assembly { - r.slot := slot - } - } - - /** - * @dev Returns an `Bytes32Slot` with member `value` located at `slot`. - */ - function getBytes32Slot(bytes32 slot) internal pure returns (Bytes32Slot storage r) { - /// @solidity memory-safe-assembly - assembly { - r.slot := slot - } - } - - /** - * @dev Returns an `Uint256Slot` with member `value` located at `slot`. - */ - function getUint256Slot(bytes32 slot) internal pure returns (Uint256Slot storage r) { - /// @solidity memory-safe-assembly - assembly { - r.slot := slot - } - } -} - -// lib/openzeppelin-contracts/contracts/proxy/ERC1967/ERC1967Upgrade.sol - -// OpenZeppelin Contracts (last updated v4.5.0) (proxy/ERC1967/ERC1967Upgrade.sol) - -/** - * @dev This abstract contract provides getters and event emitting update functions for - * https://eips.ethereum.org/EIPS/eip-1967[EIP1967] slots. - * - * _Available since v4.1._ - * - * @custom:oz-upgrades-unsafe-allow delegatecall - */ -abstract contract ERC1967Upgrade { - // This is the keccak-256 hash of "eip1967.proxy.rollback" subtracted by 1 - bytes32 private constant _ROLLBACK_SLOT = 0x4910fdfa16fed3260ed0e7147f7cc6da11a60208b5b9406d12a635614ffd9143; - - /** - * @dev Storage slot with the address of the current implementation. - * This is the keccak-256 hash of "eip1967.proxy.implementation" subtracted by 1, and is - * validated in the constructor. - */ - bytes32 internal constant _IMPLEMENTATION_SLOT = 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc; - - /** - * @dev Emitted when the implementation is upgraded. - */ - event Upgraded(address indexed implementation); - - /** - * @dev Returns the current implementation address. - */ - function _getImplementation() internal view returns (address) { - return StorageSlot.getAddressSlot(_IMPLEMENTATION_SLOT).value; - } - - /** - * @dev Stores a new address in the EIP1967 implementation slot. - */ - function _setImplementation(address newImplementation) private { - require(Address.isContract(newImplementation), "ERC1967: new implementation is not a contract"); - StorageSlot.getAddressSlot(_IMPLEMENTATION_SLOT).value = newImplementation; - } - - /** - * @dev Perform implementation upgrade - * - * Emits an {Upgraded} event. - */ - function _upgradeTo(address newImplementation) internal { - _setImplementation(newImplementation); - emit Upgraded(newImplementation); - } - - /** - * @dev Perform implementation upgrade with additional setup call. - * - * Emits an {Upgraded} event. - */ - function _upgradeToAndCall( - address newImplementation, - bytes memory data, - bool forceCall - ) internal { - _upgradeTo(newImplementation); - if (data.length > 0 || forceCall) { - Address.functionDelegateCall(newImplementation, data); - } - } - - /** - * @dev Perform implementation upgrade with security checks for UUPS proxies, and additional setup call. - * - * Emits an {Upgraded} event. - */ - function _upgradeToAndCallUUPS( - address newImplementation, - bytes memory data, - bool forceCall - ) internal { - // Upgrades from old implementations will perform a rollback test. This test requires the new - // implementation to upgrade back to the old, non-ERC1822 compliant, implementation. Removing - // this special case will break upgrade paths from old UUPS implementation to new ones. - if (StorageSlot.getBooleanSlot(_ROLLBACK_SLOT).value) { - _setImplementation(newImplementation); - } else { - try IERC1822Proxiable(newImplementation).proxiableUUID() returns (bytes32 slot) { - require(slot == _IMPLEMENTATION_SLOT, "ERC1967Upgrade: unsupported proxiableUUID"); - } catch { - revert("ERC1967Upgrade: new implementation is not UUPS"); - } - _upgradeToAndCall(newImplementation, data, forceCall); - } - } - - /** - * @dev Storage slot with the admin of the contract. - * This is the keccak-256 hash of "eip1967.proxy.admin" subtracted by 1, and is - * validated in the constructor. - */ - bytes32 internal constant _ADMIN_SLOT = 0xb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103; - - /** - * @dev Emitted when the admin account has changed. - */ - event AdminChanged(address previousAdmin, address newAdmin); - - /** - * @dev Returns the current admin. - */ - function _getAdmin() internal view returns (address) { - return StorageSlot.getAddressSlot(_ADMIN_SLOT).value; - } - - /** - * @dev Stores a new address in the EIP1967 admin slot. - */ - function _setAdmin(address newAdmin) private { - require(newAdmin != address(0), "ERC1967: new admin is the zero address"); - StorageSlot.getAddressSlot(_ADMIN_SLOT).value = newAdmin; - } - - /** - * @dev Changes the admin of the proxy. - * - * Emits an {AdminChanged} event. - */ - function _changeAdmin(address newAdmin) internal { - emit AdminChanged(_getAdmin(), newAdmin); - _setAdmin(newAdmin); - } - - /** - * @dev The storage slot of the UpgradeableBeacon contract which defines the implementation for this proxy. - * This is bytes32(uint256(keccak256('eip1967.proxy.beacon')) - 1)) and is validated in the constructor. - */ - bytes32 internal constant _BEACON_SLOT = 0xa3f0ad74e5423aebfd80d3ef4346578335a9a72aeaee59ff6cb3582b35133d50; - - /** - * @dev Emitted when the beacon is upgraded. - */ - event BeaconUpgraded(address indexed beacon); - - /** - * @dev Returns the current beacon. - */ - function _getBeacon() internal view returns (address) { - return StorageSlot.getAddressSlot(_BEACON_SLOT).value; - } - - /** - * @dev Stores a new beacon in the EIP1967 beacon slot. - */ - function _setBeacon(address newBeacon) private { - require(Address.isContract(newBeacon), "ERC1967: new beacon is not a contract"); - require( - Address.isContract(IBeacon(newBeacon).implementation()), - "ERC1967: beacon implementation is not a contract" - ); - StorageSlot.getAddressSlot(_BEACON_SLOT).value = newBeacon; - } - - /** - * @dev Perform beacon upgrade with additional setup call. Note: This upgrades the address of the beacon, it does - * not upgrade the implementation contained in the beacon (see {UpgradeableBeacon-_setImplementation} for that). - * - * Emits a {BeaconUpgraded} event. - */ - function _upgradeBeaconToAndCall( - address newBeacon, - bytes memory data, - bool forceCall - ) internal { - _setBeacon(newBeacon); - emit BeaconUpgraded(newBeacon); - if (data.length > 0 || forceCall) { - Address.functionDelegateCall(IBeacon(newBeacon).implementation(), data); - } - } -} - -// lib/openzeppelin-contracts/contracts/proxy/ERC1967/ERC1967Proxy.sol - -// OpenZeppelin Contracts (last updated v4.7.0) (proxy/ERC1967/ERC1967Proxy.sol) - -/** - * @dev This contract implements an upgradeable proxy. It is upgradeable because calls are delegated to an - * implementation address that can be changed. This address is stored in storage in the location specified by - * https://eips.ethereum.org/EIPS/eip-1967[EIP1967], so that it doesn't conflict with the storage layout of the - * implementation behind the proxy. - */ -contract ERC1967Proxy is Proxy, ERC1967Upgrade { - /** - * @dev Initializes the upgradeable proxy with an initial implementation specified by `_logic`. - * - * If `_data` is nonempty, it's used as data in a delegate call to `_logic`. This will typically be an encoded - * function call, and allows initializing the storage of the proxy like a Solidity constructor. - */ - constructor(address _logic, bytes memory _data) payable { - _upgradeToAndCall(_logic, _data, false); - } - - /** - * @dev Returns the current implementation address. - */ - function _implementation() internal view virtual override returns (address impl) { - return ERC1967Upgrade._getImplementation(); - } -} - -// lib/openzeppelin-contracts/contracts/proxy/transparent/TransparentUpgradeableProxy.sol - -// OpenZeppelin Contracts (last updated v4.7.0) (proxy/transparent/TransparentUpgradeableProxy.sol) - -/** - * @dev This contract implements a proxy that is upgradeable by an admin. - * - * To avoid https://medium.com/nomic-labs-blog/malicious-backdoors-in-ethereum-proxies-62629adf3357[proxy selector - * clashing], which can potentially be used in an attack, this contract uses the - * https://blog.openzeppelin.com/the-transparent-proxy-pattern/[transparent proxy pattern]. This pattern implies two - * things that go hand in hand: - * - * 1. If any account other than the admin calls the proxy, the call will be forwarded to the implementation, even if - * that call matches one of the admin functions exposed by the proxy itself. - * 2. If the admin calls the proxy, it can access the admin functions, but its calls will never be forwarded to the - * implementation. If the admin tries to call a function on the implementation it will fail with an error that says - * "admin cannot fallback to proxy target". - * - * These properties mean that the admin account can only be used for admin actions like upgrading the proxy or changing - * the admin, so it's best if it's a dedicated account that is not used for anything else. This will avoid headaches due - * to sudden errors when trying to call a function from the proxy implementation. - * - * Our recommendation is for the dedicated account to be an instance of the {ProxyAdmin} contract. If set up this way, - * you should think of the `ProxyAdmin` instance as the real administrative interface of your proxy. - */ -contract TransparentUpgradeableProxy is ERC1967Proxy { - /** - * @dev Initializes an upgradeable proxy managed by `_admin`, backed by the implementation at `_logic`, and - * optionally initialized with `_data` as explained in {ERC1967Proxy-constructor}. - */ - constructor( - address _logic, - address admin_, - bytes memory _data - ) payable ERC1967Proxy(_logic, _data) { - _changeAdmin(admin_); - } - - /** - * @dev Modifier used internally that will delegate the call to the implementation unless the sender is the admin. - */ - modifier ifAdmin() { - if (msg.sender == _getAdmin()) { - _; - } else { - _fallback(); - } - } - - /** - * @dev Returns the current admin. - * - * NOTE: Only the admin can call this function. See {ProxyAdmin-getProxyAdmin}. - * - * TIP: To get this value clients can read directly from the storage slot shown below (specified by EIP1967) using the - * https://eth.wiki/json-rpc/API#eth_getstorageat[`eth_getStorageAt`] RPC call. - * `0xb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103` - */ - function admin() external ifAdmin returns (address admin_) { - admin_ = _getAdmin(); - } - - /** - * @dev Returns the current implementation. - * - * NOTE: Only the admin can call this function. See {ProxyAdmin-getProxyImplementation}. - * - * TIP: To get this value clients can read directly from the storage slot shown below (specified by EIP1967) using the - * https://eth.wiki/json-rpc/API#eth_getstorageat[`eth_getStorageAt`] RPC call. - * `0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc` - */ - function implementation() external ifAdmin returns (address implementation_) { - implementation_ = _implementation(); - } - - /** - * @dev Changes the admin of the proxy. - * - * Emits an {AdminChanged} event. - * - * NOTE: Only the admin can call this function. See {ProxyAdmin-changeProxyAdmin}. - */ - function changeAdmin(address newAdmin) external virtual ifAdmin { - _changeAdmin(newAdmin); - } - - /** - * @dev Upgrade the implementation of the proxy. - * - * NOTE: Only the admin can call this function. See {ProxyAdmin-upgrade}. - */ - function upgradeTo(address newImplementation) external ifAdmin { - _upgradeToAndCall(newImplementation, bytes(""), false); - } - - /** - * @dev Upgrade the implementation of the proxy, and then call a function from the new implementation as specified - * by `data`, which should be an encoded function call. This is useful to initialize new storage variables in the - * proxied contract. - * - * NOTE: Only the admin can call this function. See {ProxyAdmin-upgradeAndCall}. - */ - function upgradeToAndCall(address newImplementation, bytes calldata data) external payable ifAdmin { - _upgradeToAndCall(newImplementation, data, true); - } - - /** - * @dev Returns the current admin. - */ - function _admin() internal view virtual returns (address) { - return _getAdmin(); - } - - /** - * @dev Makes sure the admin cannot access the fallback function. See {Proxy-_beforeFallback}. - */ - function _beforeFallback() internal virtual override { - require(msg.sender != _getAdmin(), "TransparentUpgradeableProxy: admin cannot fallback to proxy target"); - super._beforeFallback(); - } -} - From 7b96168d3ebb245b9c1d0cc36639fc6c48af2488 Mon Sep 17 00:00:00 2001 From: 0xtekgrinder <0xtekgrinder@protonmail.com> Date: Fri, 20 Sep 2024 16:59:29 +0200 Subject: [PATCH 15/69] fix: correct computation of slippage for XEVT --- contracts/helpers/MultiBlockRebalancer.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/helpers/MultiBlockRebalancer.sol b/contracts/helpers/MultiBlockRebalancer.sol index ffcbdac0..2fa19ea7 100644 --- a/contracts/helpers/MultiBlockRebalancer.sol +++ b/contracts/helpers/MultiBlockRebalancer.sol @@ -198,7 +198,7 @@ contract MultiBlockRebalancer is BaseRebalancer { if (slippage > maxSlippage) revert SlippageTooHigh(); } else if (collateral == XEVT) { // Assumer 1:1 ratio between the underlying asset of the vault - uint256 slippage = ((amountIn - IPool(depositAddress).convertToAssets(amountOut)) * 1e9) / amountIn; + uint256 slippage = ((IPool(depositAddress).convertToAssets(amountIn) - amountOut) * 1e9) / amountIn; if (slippage > maxSlippage) revert SlippageTooHigh(); } else revert InvalidParam(); } From c3da3ef0de20b693f919a2a07f590f4ea2d7df59 Mon Sep 17 00:00:00 2001 From: 0xtekgrinder <0xtekgrinder@protonmail.com> Date: Fri, 20 Sep 2024 17:37:08 +0200 Subject: [PATCH 16/69] feat: specify balance in finalizeRebalance function --- contracts/helpers/MultiBlockRebalancer.sol | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/contracts/helpers/MultiBlockRebalancer.sol b/contracts/helpers/MultiBlockRebalancer.sol index 2fa19ea7..bbd6a29f 100644 --- a/contracts/helpers/MultiBlockRebalancer.sol +++ b/contracts/helpers/MultiBlockRebalancer.sol @@ -99,9 +99,7 @@ contract MultiBlockRebalancer is BaseRebalancer { * @notice Finalize a rebalance * @param collateral address of the collateral */ - function finalizeRebalance(address collateral) external onlyTrusted { - uint256 balance = IERC20(collateral).balanceOf(address(this)); - + function finalizeRebalance(address collateral, uint256 balance) external onlyTrusted { try transmuter.updateOracle(collateral) {} catch {} _adjustAllowance(address(agToken), address(transmuter), balance); uint256 amountOut = transmuter.swapExactInput( From 88210b904dcf5a4874622a794ab1754a2f52a683 Mon Sep 17 00:00:00 2001 From: 0xtekgrinder <0xtekgrinder@protonmail.com> Date: Fri, 20 Sep 2024 18:20:04 +0200 Subject: [PATCH 17/69] refactor: rename swap arguments --- contracts/helpers/GenericHarvester.sol | 28 +++++++++++++------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/contracts/helpers/GenericHarvester.sol b/contracts/helpers/GenericHarvester.sol index 4fc347cd..0017f09c 100644 --- a/contracts/helpers/GenericHarvester.sol +++ b/contracts/helpers/GenericHarvester.sol @@ -135,7 +135,7 @@ contract GenericHarvester is BaseRebalancer, IERC3156FlashBorrower, RouterSwappe ); // Swap to tokenIn - amountOut = _swapToTokenIn(typeAction, tokenIn, tokenOut, amountOut, swapType, callData); + amountOut = _swapToTokenOut(typeAction, tokenOut, tokenIn, amountOut, swapType, callData); _adjustAllowance(tokenIn, address(transmuter), amountOut); uint256 amountStableOut = transmuter.swapExactInput( @@ -176,7 +176,7 @@ contract GenericHarvester is BaseRebalancer, IERC3156FlashBorrower, RouterSwappe IERC20(agToken).safeTransfer(receiver, amount); } - function _swapToTokenIn( + function _swapToTokenOut( uint256 typeAction, address tokenIn, address tokenOut, @@ -198,30 +198,30 @@ contract GenericHarvester is BaseRebalancer, IERC3156FlashBorrower, RouterSwappe * @param amount amount of token to swap * @param callData bytes to call the router/aggregator */ - function _swapToTokenInSwap( + function _swapToTokenOutSwap( address tokenIn, address tokenOut, uint256 amount, bytes memory callData ) internal returns (uint256) { - uint256 balance = IERC20(tokenIn).balanceOf(address(this)); + uint256 balance = IERC20(tokenOut).balanceOf(address(this)); address[] memory tokens = new address[](1); - tokens[0] = tokenOut; + tokens[0] = tokenIn; 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 amountOut = IERC20(tokenOut).balanceOf(address(this)) - balance; uint256 decimalsTokenIn = IERC20Metadata(tokenIn).decimals(); + uint256 decimalsTokenOut = IERC20Metadata(tokenOut).decimals(); - if (decimalsTokenOut > decimalsTokenIn) { - amount /= 10 ** (decimalsTokenOut - decimalsTokenIn); - } else if (decimalsTokenOut < decimalsTokenIn) { - amount *= 10 ** (decimalsTokenIn - decimalsTokenOut); + if (decimalsTokenIn > decimalsTokenOut) { + amount /= 10 ** (decimalsTokenIn - decimalsTokenOut); + } else if (decimalsTokenIn < decimalsTokenOut) { + amount *= 10 ** (decimalsTokenOut - decimalsTokenIn); } if (amountOut < (amount * (BPS - maxSwapSlippage)) / BPS) { revert SlippageTooHigh(); @@ -236,7 +236,7 @@ contract GenericHarvester is BaseRebalancer, IERC3156FlashBorrower, RouterSwappe * @param tokenOut address of the token to receive * @param amount amount of token to swap */ - function _swapToTokenInVault( + function _swapToTokenOutVault( uint256 typeAction, address tokenIn, address tokenOut, @@ -244,8 +244,8 @@ contract GenericHarvester is BaseRebalancer, IERC3156FlashBorrower, RouterSwappe ) internal 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)); + _adjustAllowance(tokenOut, tokenOut, amount); + amountOut = IERC4626(tokenOut).deposit(amount, address(this)); } else amountOut = IERC4626(tokenOut).redeem(amount, address(this), address(this)); } From a4f4b8587a52d2ae33953ae12896e0e2bfd6fae4 Mon Sep 17 00:00:00 2001 From: 0xtekgrinder <0xtekgrinder@protonmail.com> Date: Mon, 23 Sep 2024 15:24:36 +0200 Subject: [PATCH 18/69] refactor: change mapping of stablecoin to yieldbearing to the oposite --- .../{BaseRebalancer.sol => BaseHarvester.sol} | 139 ++++++++++-------- contracts/helpers/GenericHarvester.sol | 65 ++++---- ...Rebalancer.sol => MultiBlockHarvester.sol} | 104 +++++++------ ....s.sol => DeployMultiBlockHarvester.s.sol} | 6 +- 4 files changed, 167 insertions(+), 147 deletions(-) rename contracts/helpers/{BaseRebalancer.sol => BaseHarvester.sol} (60%) rename contracts/helpers/{MultiBlockRebalancer.sol => MultiBlockHarvester.sol} (66%) rename scripts/{DeployMultiBlockRebalancer.s.sol => DeployMultiBlockHarvester.s.sol} (86%) diff --git a/contracts/helpers/BaseRebalancer.sol b/contracts/helpers/BaseHarvester.sol similarity index 60% rename from contracts/helpers/BaseRebalancer.sol rename to contracts/helpers/BaseHarvester.sol index 4e23e0e6..3fc553b1 100644 --- a/contracts/helpers/BaseRebalancer.sol +++ b/contracts/helpers/BaseHarvester.sol @@ -10,21 +10,24 @@ import { IAgToken } from "../interfaces/IAgToken.sol"; import "../utils/Errors.sol"; -struct CollatParams { - // Address of the collateral - address asset; - // Target exposure to the collateral asset used +struct YieldBearingParams { + // Address of the stablecoin (ex: USDC) + address stablecoin; + // Target exposure to the collateral yield bearing asset used uint64 targetExposure; - // Maximum exposure within the Transmuter to the asset + // Maximum exposure within the Transmuter to the yield bearing asset uint64 maxExposureYieldAsset; - // Minimum exposure within the Transmuter to the asset + // Minimum exposure within the Transmuter to the yield bearing 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; } -abstract contract BaseRebalancer is AccessControl { +/// @title BaseHarvester +/// @author Angle Labs, Inc. +/// @dev Abstract contract for a harvester that aims at rebalancing a Transmuter +abstract contract BaseHarvester is AccessControl { using SafeERC20 for IERC20; /*////////////////////////////////////////////////////////////////////////////////////////////////////////////////// @@ -37,8 +40,8 @@ abstract contract BaseRebalancer is AccessControl { IAgToken public immutable agToken; /// @notice Max slippage when dealing with the Transmuter uint96 public maxSlippage; - /// @notice Data associated to a collateral - mapping(address => CollatParams) public collateralData; + /// @notice Data associated to a yield bearing asset + mapping(address => YieldBearingParams) public yieldBearingData; /*////////////////////////////////////////////////////////////////////////////////////////////////////////////////// CONSTRUCTOR @@ -61,24 +64,24 @@ abstract contract BaseRebalancer is AccessControl { //////////////////////////////////////////////////////////////////////////////////////////////////////////////////*/ /** - * @notice Set the collateral data - * @param collateral address of the collateral - * @param targetExposure target exposure to the collateral asset used + * @notice Set the yieldBearingAsset data + * @param yieldBearingAsset address of the yieldBearingAsset + * @param targetExposure target exposure to the yieldBearingAsset asset used * @param minExposureYieldAsset minimum exposure within the Transmuter to the asset * @param maxExposureYieldAsset maximum exposure within the Transmuter to the asset * @param overrideExposures 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 */ - function setCollateralData( - address collateral, + function setYieldBearingAssetData( + address yieldBearingAsset, uint64 targetExposure, uint64 minExposureYieldAsset, uint64 maxExposureYieldAsset, uint64 overrideExposures ) external onlyGuardian { - _setCollateralData( - collateral, - collateral, + _setYieldBearingAssetData( + yieldBearingAsset, + yieldBearingAsset, targetExposure, minExposureYieldAsset, maxExposureYieldAsset, @@ -87,25 +90,25 @@ abstract contract BaseRebalancer is AccessControl { } /** - * @notice Set the collateral data - * @param collateral address of the collateral - * @param asset address of the asset - * @param targetExposure target exposure to the collateral asset used + * @notice Set the yieldBearingAsset data + * @param yieldBearingAsset address of the yieldBearingAsset + * @param stablecoin address of the stablecoin + * @param targetExposure target exposure to the yieldBearingAsset asset used * @param minExposureYieldAsset minimum exposure within the Transmuter to the asset * @param maxExposureYieldAsset maximum exposure within the Transmuter to the asset * @param overrideExposures whether limit exposures should be overriden or read onchain through the Transmuter */ - function setCollateralData( - address collateral, - address asset, + function setYieldBearingAssetData( + address yieldBearingAsset, + address stablecoin, uint64 targetExposure, uint64 minExposureYieldAsset, uint64 maxExposureYieldAsset, uint64 overrideExposures ) external onlyGuardian { - _setCollateralData( - collateral, - asset, + _setYieldBearingAssetData( + yieldBearingAsset, + stablecoin, targetExposure, minExposureYieldAsset, maxExposureYieldAsset, @@ -115,11 +118,12 @@ abstract contract BaseRebalancer is AccessControl { /** * @notice Set the limit exposures to the yield bearing asset - * @param collateral address of the collateral + * @param yieldBearingAsset address of the yield bearing asset */ - function updateLimitExposuresYieldAsset(address collateral) public virtual onlyGuardian { - CollatParams storage collatInfo = collateralData[collateral]; - if (collatInfo.overrideExposures == 2) _updateLimitExposuresYieldAsset(collatInfo); + function updateLimitExposuresYieldAsset(address yieldBearingAsset) public virtual onlyGuardian { + YieldBearingParams storage yieldBearingInfo = yieldBearingData[yieldBearingAsset]; + if (yieldBearingInfo.overrideExposures == 2) + _updateLimitExposuresYieldAsset(yieldBearingAsset, yieldBearingInfo); } /** @@ -135,68 +139,73 @@ abstract contract BaseRebalancer is AccessControl { //////////////////////////////////////////////////////////////////////////////////////////////////////////////////*/ function _computeRebalanceAmount( - address collateral, - CollatParams memory collatInfo + address yieldBearingAsset, + YieldBearingParams memory yieldBearingInfo ) internal view returns (uint8 increase, uint256 amount) { - (uint256 stablecoinsFromCollateral, uint256 stablecoinsIssued) = transmuter.getIssuedByCollateral(collateral); - (uint256 stablecoinsFromAsset, ) = transmuter.getIssuedByCollateral(collatInfo.asset); - uint256 targetExposureScaled = collatInfo.targetExposure * stablecoinsIssued; - if (stablecoinsFromCollateral * 1e9 > targetExposureScaled) { + (uint256 stablecoinsFromYieldBearingAsset, uint256 stablecoinsIssued) = transmuter.getIssuedByCollateral( + yieldBearingAsset + ); + (uint256 stablecoinsFromStablecoin, ) = transmuter.getIssuedByCollateral(yieldBearingInfo.stablecoin); + uint256 targetExposureScaled = yieldBearingInfo.targetExposure * stablecoinsIssued; + if (stablecoinsFromYieldBearingAsset * 1e9 > targetExposureScaled) { // Need to increase exposure to yield bearing asset increase = 1; - amount = stablecoinsFromCollateral - targetExposureScaled / 1e9; - uint256 maxValueScaled = collatInfo.maxExposureYieldAsset * stablecoinsIssued; + amount = stablecoinsFromYieldBearingAsset - targetExposureScaled / 1e9; + uint256 maxValueScaled = yieldBearingInfo.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 (stablecoinsFromAsset * 1e9 > maxValueScaled) amount = 0; - else if ((stablecoinsFromAsset + amount) * 1e9 > maxValueScaled) - amount = maxValueScaled / 1e9 - stablecoinsFromAsset; + if (stablecoinsFromStablecoin * 1e9 > maxValueScaled) amount = 0; + else if ((stablecoinsFromStablecoin + amount) * 1e9 > maxValueScaled) + amount = maxValueScaled / 1e9 - stablecoinsFromStablecoin; } 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 (stablecoinsFromAsset * 1e9 < minValueScaled) amount = 0; - else if (stablecoinsFromAsset * 1e9 < minValueScaled + amount * 1e9) - amount = stablecoinsFromAsset - minValueScaled / 1e9; + amount = targetExposureScaled / 1e9 - stablecoinsFromYieldBearingAsset; + uint256 minValueScaled = yieldBearingInfo.minExposureYieldAsset * stablecoinsIssued; + if (stablecoinsFromStablecoin * 1e9 < minValueScaled) amount = 0; + else if (stablecoinsFromStablecoin * 1e9 < minValueScaled + amount * 1e9) + amount = stablecoinsFromStablecoin - minValueScaled / 1e9; } } - function _setCollateralData( - address collateral, - address asset, + function _setYieldBearingAssetData( + address yieldBearingAsset, + address stablecoin, uint64 targetExposure, uint64 minExposureYieldAsset, uint64 maxExposureYieldAsset, uint64 overrideExposures ) internal virtual { - CollatParams storage collatInfo = collateralData[collateral]; - collatInfo.asset = asset; + YieldBearingParams storage yieldBearingInfo = yieldBearingData[yieldBearingAsset]; + yieldBearingInfo.stablecoin = stablecoin; if (targetExposure >= 1e9) revert InvalidParam(); - collatInfo.targetExposure = targetExposure; - collatInfo.overrideExposures = overrideExposures; + yieldBearingInfo.targetExposure = targetExposure; + yieldBearingInfo.overrideExposures = overrideExposures; if (overrideExposures == 1) { if (maxExposureYieldAsset >= 1e9 || minExposureYieldAsset >= maxExposureYieldAsset) revert InvalidParam(); - collatInfo.maxExposureYieldAsset = maxExposureYieldAsset; - collatInfo.minExposureYieldAsset = minExposureYieldAsset; + yieldBearingInfo.maxExposureYieldAsset = maxExposureYieldAsset; + yieldBearingInfo.minExposureYieldAsset = minExposureYieldAsset; } else { - collatInfo.overrideExposures = 2; - _updateLimitExposuresYieldAsset(collatInfo); + yieldBearingInfo.overrideExposures = 2; + _updateLimitExposuresYieldAsset(yieldBearingAsset, yieldBearingInfo); } } - function _updateLimitExposuresYieldAsset(CollatParams storage collatInfo) internal virtual { + function _updateLimitExposuresYieldAsset( + address yieldBearingAsset, + YieldBearingParams storage yieldBearingInfo + ) internal virtual { uint64[] memory xFeeMint; - (xFeeMint, ) = transmuter.getCollateralMintFees(collatInfo.asset); + (xFeeMint, ) = transmuter.getCollateralMintFees(yieldBearingAsset); uint256 length = xFeeMint.length; - if (length <= 1) collatInfo.maxExposureYieldAsset = 1e9; - else collatInfo.maxExposureYieldAsset = xFeeMint[length - 2]; + if (length <= 1) yieldBearingInfo.maxExposureYieldAsset = 1e9; + else yieldBearingInfo.maxExposureYieldAsset = xFeeMint[length - 2]; uint64[] memory xFeeBurn; - (xFeeBurn, ) = transmuter.getCollateralBurnFees(collatInfo.asset); + (xFeeBurn, ) = transmuter.getCollateralBurnFees(yieldBearingAsset); length = xFeeBurn.length; - if (length <= 1) collatInfo.minExposureYieldAsset = 0; - else collatInfo.minExposureYieldAsset = xFeeBurn[length - 2]; + if (length <= 1) yieldBearingInfo.minExposureYieldAsset = 0; + else yieldBearingInfo.minExposureYieldAsset = xFeeBurn[length - 2]; } function _setMaxSlippage(uint96 newMaxSlippage) internal virtual { diff --git a/contracts/helpers/GenericHarvester.sol b/contracts/helpers/GenericHarvester.sol index 0017f09c..a5134720 100644 --- a/contracts/helpers/GenericHarvester.sol +++ b/contracts/helpers/GenericHarvester.sol @@ -18,7 +18,7 @@ import "../utils/Errors.sol"; import { IERC3156FlashBorrower } from "oz/interfaces/IERC3156FlashBorrower.sol"; import { IERC3156FlashLender } from "oz/interfaces/IERC3156FlashLender.sol"; import { IERC4626 } from "interfaces/external/IERC4626.sol"; -import { BaseRebalancer, CollatParams } from "./BaseRebalancer.sol"; +import { BaseHarvester, YieldBearingParams } from "./BaseHarvester.sol"; enum SwapType { VAULT, @@ -28,7 +28,7 @@ enum SwapType { /// @title GenericHarvester /// @author Angle Labs, Inc. /// @dev Generic contract for anyone to permissionlessly adjust the reserves of Angle Transmuter -contract GenericHarvester is BaseRebalancer, IERC3156FlashBorrower, RouterSwapper { +contract GenericHarvester is BaseHarvester, IERC3156FlashBorrower, RouterSwapper { using SafeCast for uint256; using SafeERC20 for IERC20; @@ -56,7 +56,7 @@ contract GenericHarvester is BaseRebalancer, IERC3156FlashBorrower, RouterSwappe IERC3156FlashLender definitiveFlashloan ) RouterSwapper(initialSwapRouter, initialTokenTransferAddress) - BaseRebalancer(initialMaxSlippage, definitiveAccessControlManager, definitiveAgToken, definitiveTransmuter) + BaseHarvester(initialMaxSlippage, definitiveAccessControlManager, definitiveAgToken, definitiveTransmuter) { if (address(definitiveFlashloan) == address(0)) revert ZeroAddress(); flashloan = definitiveFlashloan; @@ -69,17 +69,17 @@ contract GenericHarvester is BaseRebalancer, IERC3156FlashBorrower, RouterSwappe REBALANCE //////////////////////////////////////////////////////////////////////////////////////////////////////////////////*/ - /// @notice Burns `amountStablecoins` for one collateral asset, swap for asset then mints stablecoins + /// @notice Burns `amountStablecoins` for one yieldBearing 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, swapping for the yield bearing asset, then minting the stablecoin + /// burning stablecoin for the liquid stablecoin, 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 asset, + address yieldBearingAsset, + address stablecoin, uint256 minAmountOut, SwapType swapType, bytes calldata extraData @@ -88,7 +88,7 @@ contract GenericHarvester is BaseRebalancer, IERC3156FlashBorrower, RouterSwappe IERC3156FlashBorrower(address(this)), address(agToken), amountStablecoins, - abi.encode(msg.sender, increase, collateral, asset, minAmountOut, swapType, extraData) + abi.encode(msg.sender, increase, yieldBearingAsset, stablecoin, minAmountOut, swapType, extraData) ); } @@ -109,20 +109,20 @@ contract GenericHarvester is BaseRebalancer, IERC3156FlashBorrower, RouterSwappe address tokenOut; address tokenIn; { - address collateral; - address asset; - (sender, typeAction, collateral, asset, minAmountOut, swapType, callData) = abi.decode( + address yieldBearingAsset; + address stablecoin; + (sender, typeAction, yieldBearingAsset, stablecoin, minAmountOut, swapType, callData) = abi.decode( data, (address, uint256, address, address, uint256, SwapType, bytes) ); if (typeAction == 1) { // Increase yield exposure action: we bring in the yield bearing asset - tokenOut = collateral; - tokenIn = asset; + tokenOut = yieldBearingAsset; + tokenIn = stablecoin; } else { - // Decrease yield exposure action: we bring in the liquid asset - tokenIn = collateral; - tokenOut = asset; + // Decrease yield exposure action: we bring in the liquid stablecoin + tokenIn = yieldBearingAsset; + tokenOut = stablecoin; } } uint256 amountOut = transmuter.swapExactInput( @@ -185,9 +185,9 @@ contract GenericHarvester is BaseRebalancer, IERC3156FlashBorrower, RouterSwappe bytes memory callData ) internal returns (uint256 amountOut) { if (swapType == SwapType.SWAP) { - amountOut = _swapToTokenInSwap(tokenIn, tokenOut, amount, callData); + amountOut = _swapToTokenOutSwap(tokenIn, tokenOut, amount, callData); } else if (swapType == SwapType.VAULT) { - amountOut = _swapToTokenInVault(typeAction, tokenIn, tokenOut, amount); + amountOut = _swapToTokenOutVault(typeAction, tokenOut, amount); } } @@ -232,18 +232,16 @@ contract GenericHarvester is BaseRebalancer, IERC3156FlashBorrower, RouterSwappe /** * @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 _swapToTokenOutVault( uint256 typeAction, - address tokenIn, address tokenOut, uint256 amount ) internal returns (uint256 amountOut) { if (typeAction == 1) { - // Granting allowance with the collateral for the vault asset + // Granting allowance with the yieldBearingAsset for the vault asset _adjustAllowance(tokenOut, tokenOut, amount); amountOut = IERC4626(tokenOut).deposit(amount, address(this)); } else amountOut = IERC4626(tokenOut).redeem(amount, address(this), address(this)); @@ -253,26 +251,31 @@ contract GenericHarvester is BaseRebalancer, IERC3156FlashBorrower, RouterSwappe HARVEST //////////////////////////////////////////////////////////////////////////////////////////////////////////////////*/ - /// @notice Invests or divests from the yield asset associated to `collateral` based on the current exposure to this - /// collateral - /// @dev This transaction either reduces the exposure to `collateral` in the Transmuter or frees up some collateral + /// @notice Invests or divests from the yield asset associated to `yieldBearingAsset` based on the current exposure to this + /// yieldBearingAsset + /// @dev This transaction either reduces the exposure to `yieldBearingAsset` in the Transmuter or frees up some yieldBearingAsset /// 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` + /// @dev Due to potential transaction fees within the Transmuter, this function doesn't exactly bring `yieldBearingAsset` /// to the target exposure - function harvest(address collateral, uint256 scale, SwapType swapType, bytes calldata extraData) public virtual { + function harvest( + address yieldBearingAsset, + uint256 scale, + SwapType swapType, + bytes calldata extraData + ) public virtual { if (scale > 1e9) revert InvalidParam(); - CollatParams memory collatInfo = collateralData[collateral]; - (uint8 increase, uint256 amount) = _computeRebalanceAmount(collateral, collatInfo); + YieldBearingParams memory yieldBearingInfo = yieldBearingData[yieldBearingAsset]; + (uint8 increase, uint256 amount) = _computeRebalanceAmount(yieldBearingAsset, yieldBearingInfo); amount = (amount * scale) / 1e9; if (amount > 0) { - try transmuter.updateOracle(collatInfo.asset) {} catch {} + try transmuter.updateOracle(yieldBearingInfo.stablecoin) {} catch {} adjustYieldExposure( amount, increase, - collateral, - collatInfo.asset, + yieldBearingAsset, + yieldBearingInfo.stablecoin, (amount * (1e9 - maxSlippage)) / 1e9, swapType, extraData diff --git a/contracts/helpers/MultiBlockRebalancer.sol b/contracts/helpers/MultiBlockHarvester.sol similarity index 66% rename from contracts/helpers/MultiBlockRebalancer.sol rename to contracts/helpers/MultiBlockHarvester.sol index bbd6a29f..add6ace8 100644 --- a/contracts/helpers/MultiBlockRebalancer.sol +++ b/contracts/helpers/MultiBlockHarvester.sol @@ -5,7 +5,7 @@ pragma solidity ^0.8.23; import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import { AccessControl, IAccessControlManager } from "../utils/AccessControl.sol"; import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; -import { BaseRebalancer, CollatParams } from "./BaseRebalancer.sol"; +import { BaseHarvester, YieldBearingParams } from "./BaseHarvester.sol"; import { ITransmuter } from "../interfaces/ITransmuter.sol"; import { IAgToken } from "../interfaces/IAgToken.sol"; import { IPool } from "../interfaces/IPool.sol"; @@ -13,7 +13,10 @@ import { IPool } from "../interfaces/IPool.sol"; import "../utils/Errors.sol"; import "../utils/Constants.sol"; -contract MultiBlockRebalancer is BaseRebalancer { +/// @title MultiBlockHarvester +/// @author Angle Labs, Inc. +/// @dev Contract to harvest yield from multiple yield bearing assets in multiple blocks transactions +contract MultiBlockHarvester is BaseHarvester { using SafeERC20 for IERC20; /*////////////////////////////////////////////////////////////////////////////////////////////////////////////////// @@ -29,8 +32,8 @@ contract MultiBlockRebalancer is BaseRebalancer { VARIABLES //////////////////////////////////////////////////////////////////////////////////////////////////////////////////*/ - /// @notice address to deposit to receive collateral - mapping(address => address) public collateralToDepositAddress; + /// @notice address to deposit to receive yieldBearingAsset + mapping(address => address) public yieldBearingToDepositAddress; /// @notice trusted addresses mapping(address => bool) public isTrusted; @@ -47,7 +50,7 @@ contract MultiBlockRebalancer is BaseRebalancer { IAccessControlManager definitiveAccessControlManager, IAgToken definitiveAgToken, ITransmuter definitiveTransmuter - ) BaseRebalancer(initialMaxSlippage, definitiveAccessControlManager, definitiveAgToken, definitiveTransmuter) { + ) BaseHarvester(initialMaxSlippage, definitiveAccessControlManager, definitiveAgToken, definitiveTransmuter) { maxMintAmount = initialMaxMintAmount; } @@ -68,12 +71,15 @@ contract MultiBlockRebalancer is BaseRebalancer { //////////////////////////////////////////////////////////////////////////////////////////////////////////////////*/ /** - * @notice Set the deposit address for a collateral - * @param collateral address of the collateral - * @param newDepositAddress address to deposit to receive collateral + * @notice Set the deposit address for a yieldBearingAsset + * @param yieldBearingAsset address of the yieldBearingAsset + * @param newDepositAddress address to deposit to receive yieldBearingAsset */ - function setCollateralToDepositAddress(address collateral, address newDepositAddress) external onlyGuardian { - collateralToDepositAddress[collateral] = newDepositAddress; + function setYieldBearingToDepositAddress( + address yieldBearingAsset, + address newDepositAddress + ) external onlyGuardian { + yieldBearingToDepositAddress[yieldBearingAsset] = newDepositAddress; } /*////////////////////////////////////////////////////////////////////////////////////////////////////////////////// @@ -83,35 +89,37 @@ contract MultiBlockRebalancer is BaseRebalancer { /** * @notice Initiate a rebalance * @param scale scale to apply to the rebalance amount - * @param collateral address of the collateral + * @param yieldBearingAsset address of the yieldBearingAsset */ - function initiateRebalance(uint256 scale, address collateral) external onlyTrusted { + function initiateRebalance(uint256 scale, address yieldBearingAsset) external onlyTrusted { if (scale > 1e9) revert InvalidParam(); - CollatParams memory collatInfo = collateralData[collateral]; - (uint8 increase, uint256 amount) = _computeRebalanceAmount(collateral, collatInfo); + YieldBearingParams memory yieldBearingInfo = yieldBearingData[yieldBearingAsset]; + (uint8 increase, uint256 amount) = _computeRebalanceAmount(yieldBearingAsset, yieldBearingInfo); amount = (amount * scale) / 1e9; - try transmuter.updateOracle(collateral) {} catch {} - _rebalance(increase, collateral, amount); + try transmuter.updateOracle(yieldBearingAsset) {} catch {} + _rebalance(increase, yieldBearingAsset, yieldBearingInfo, amount); } /** * @notice Finalize a rebalance - * @param collateral address of the collateral + * @param yieldBearingAsset address of the yieldBearingAsset */ - function finalizeRebalance(address collateral, uint256 balance) external onlyTrusted { - try transmuter.updateOracle(collateral) {} catch {} + function finalizeRebalance(address yieldBearingAsset, uint256 balance) external onlyTrusted { + try transmuter.updateOracle(yieldBearingAsset) {} catch {} _adjustAllowance(address(agToken), address(transmuter), balance); uint256 amountOut = transmuter.swapExactInput( balance, 0, - collateral, + yieldBearingAsset, address(agToken), address(this), block.timestamp ); - address depositAddress = collateral == XEVT ? collateralToDepositAddress[collateral] : address(0); - _checkSlippage(balance, amountOut, collateral, depositAddress); + address depositAddress = yieldBearingAsset == XEVT + ? yieldBearingToDepositAddress[yieldBearingAsset] + : address(0); + _checkSlippage(balance, amountOut, yieldBearingAsset, depositAddress); agToken.burnSelf(amountOut, address(this)); } @@ -119,63 +127,68 @@ contract MultiBlockRebalancer is BaseRebalancer { INTERNAL FUNCTIONS //////////////////////////////////////////////////////////////////////////////////////////////////////////////////*/ - function _rebalance(uint8 typeAction, address collateral, uint256 amount) internal { + function _rebalance( + uint8 typeAction, + address yieldBearingAsset, + YieldBearingParams memory yieldBearingInfo, + uint256 amount + ) internal { if (amount > maxMintAmount) revert TooBigAmountIn(); agToken.mint(address(this), amount); _adjustAllowance(address(agToken), address(transmuter), amount); if (typeAction == 1) { - address depositAddress = collateralToDepositAddress[collateral]; + address depositAddress = yieldBearingToDepositAddress[yieldBearingAsset]; - if (collateral == XEVT) { + if (yieldBearingAsset == XEVT) { uint256 amountOut = transmuter.swapExactInput( amount, 0, address(agToken), - EURC, + yieldBearingInfo.stablecoin, address(this), block.timestamp ); - _checkSlippage(amount, amountOut, collateral, depositAddress); - _adjustAllowance(collateral, address(depositAddress), amountOut); + _checkSlippage(amount, amountOut, yieldBearingInfo.stablecoin, depositAddress); + _adjustAllowance(yieldBearingInfo.stablecoin, address(depositAddress), amountOut); (uint256 shares, ) = IPool(depositAddress).deposit(amountOut, address(this)); - _adjustAllowance(collateral, address(transmuter), shares); + _adjustAllowance(yieldBearingAsset, address(transmuter), shares); amountOut = transmuter.swapExactInput( shares, 0, - collateral, + yieldBearingAsset, address(agToken), address(this), block.timestamp ); agToken.burnSelf(amountOut, address(this)); - } else if (collateral == USDM) { + } else if (yieldBearingAsset == USDM) { uint256 amountOut = transmuter.swapExactInput( amount, 0, address(agToken), - USDC, + yieldBearingInfo.stablecoin, address(this), block.timestamp ); - _checkSlippage(amount, amountOut, collateral, depositAddress); - IERC20(collateral).safeTransfer(depositAddress, amountOut); + _checkSlippage(amount, amountOut, yieldBearingInfo.stablecoin, depositAddress); + IERC20(yieldBearingInfo.stablecoin).safeTransfer(depositAddress, amountOut); } } else { uint256 amountOut = transmuter.swapExactInput( amount, 0, address(agToken), - collateral, + yieldBearingAsset, address(this), block.timestamp ); - address depositAddress = collateralToDepositAddress[collateral]; - _checkSlippage(amount, amountOut, collateral, depositAddress); + address depositAddress = yieldBearingToDepositAddress[yieldBearingAsset]; + _checkSlippage(amount, amountOut, yieldBearingAsset, depositAddress); - if (collateral == XEVT) { + if (yieldBearingAsset == XEVT) { IPool(depositAddress).requestRedeem(amountOut); - } else if (collateral == USDM) { - IERC20(collateral).safeTransfer(depositAddress, amountOut); + } else if (yieldBearingAsset == USDM) { + IERC20(yieldBearingAsset).safeTransfer(depositAddress, amountOut); } } } @@ -184,17 +197,12 @@ contract MultiBlockRebalancer is BaseRebalancer { HELPER //////////////////////////////////////////////////////////////////////////////////////////////////////////////////*/ - function _checkSlippage( - uint256 amountIn, - uint256 amountOut, - address collateral, - address depositAddress - ) internal view { - if (collateral == USDC || collateral == USDM) { + function _checkSlippage(uint256 amountIn, uint256 amountOut, address asset, address depositAddress) internal view { + if (asset == USDC || asset == USDM) { // Assume 1:1 ratio between stablecoins uint256 slippage = ((amountIn - amountOut) * 1e9) / amountIn; if (slippage > maxSlippage) revert SlippageTooHigh(); - } else if (collateral == XEVT) { + } else if (asset == XEVT) { // Assumer 1:1 ratio between the underlying asset of the vault uint256 slippage = ((IPool(depositAddress).convertToAssets(amountIn) - amountOut) * 1e9) / amountIn; if (slippage > maxSlippage) revert SlippageTooHigh(); diff --git a/scripts/DeployMultiBlockRebalancer.s.sol b/scripts/DeployMultiBlockHarvester.s.sol similarity index 86% rename from scripts/DeployMultiBlockRebalancer.s.sol rename to scripts/DeployMultiBlockHarvester.s.sol index 47a6890d..a38a8c6f 100644 --- a/scripts/DeployMultiBlockRebalancer.s.sol +++ b/scripts/DeployMultiBlockHarvester.s.sol @@ -3,13 +3,13 @@ pragma solidity ^0.8.19; import "./utils/Utils.s.sol"; import { console } from "forge-std/console.sol"; -import { MultiBlockRebalancer } from "contracts/helpers/MultiBlockRebalancer.sol"; +import { MultiBlockHarvester } from "contracts/helpers/MultiBlockHarvester.sol"; import { IAccessControlManager } from "contracts/utils/AccessControl.sol"; import { IAgToken } from "contracts/interfaces/IAgToken.sol"; import { ITransmuter } from "contracts/interfaces/ITransmuter.sol"; import "./Constants.s.sol"; -contract DeployMultiBlockRebalancer is Utils { +contract DeployMultiBlockHarvester is Utils { function run() external { uint256 deployerPrivateKey = vm.envUint("DEPLOYER_PRIVATE_KEY"); vm.startBroadcast(deployerPrivateKey); @@ -22,7 +22,7 @@ contract DeployMultiBlockRebalancer is Utils { address transmuter = _chainToContract(CHAIN_SOURCE, ContractType.TransmuterAgEUR); IAccessControlManager accessControlManager = ITransmuter(transmuter).accessControlManager(); - MultiBlockRebalancer harvester = new MultiBlockRebalancer( + MultiBlockHarvester harvester = new MultiBlockHarvester( maxMintAmount, maxSlippage, accessControlManager, From 00ee73830c2e7d6b45a1f667b2e45a443785a433 Mon Sep 17 00:00:00 2001 From: 0xtekgrinder <0xtekgrinder@protonmail.com> Date: Mon, 23 Sep 2024 15:29:26 +0200 Subject: [PATCH 19/69] feat: harvest function for both harvester contracts --- contracts/helpers/BaseHarvester.sol | 3 ++- contracts/helpers/GenericHarvester.sol | 13 ++++------- contracts/helpers/MultiBlockHarvester.sol | 2 +- contracts/interfaces/IHarvester.sol | 28 +++++++++++++++++++++++ 4 files changed, 36 insertions(+), 10 deletions(-) create mode 100644 contracts/interfaces/IHarvester.sol diff --git a/contracts/helpers/BaseHarvester.sol b/contracts/helpers/BaseHarvester.sol index 3fc553b1..3bf1d711 100644 --- a/contracts/helpers/BaseHarvester.sol +++ b/contracts/helpers/BaseHarvester.sol @@ -9,6 +9,7 @@ import { ITransmuter } from "../interfaces/ITransmuter.sol"; import { IAgToken } from "../interfaces/IAgToken.sol"; import "../utils/Errors.sol"; +import "../interfaces/IHarvester.sol"; struct YieldBearingParams { // Address of the stablecoin (ex: USDC) @@ -27,7 +28,7 @@ struct YieldBearingParams { /// @title BaseHarvester /// @author Angle Labs, Inc. /// @dev Abstract contract for a harvester that aims at rebalancing a Transmuter -abstract contract BaseHarvester is AccessControl { +abstract contract BaseHarvester is IHarvester, AccessControl { using SafeERC20 for IERC20; /*////////////////////////////////////////////////////////////////////////////////////////////////////////////////// diff --git a/contracts/helpers/GenericHarvester.sol b/contracts/helpers/GenericHarvester.sol index a5134720..6c11cd8f 100644 --- a/contracts/helpers/GenericHarvester.sol +++ b/contracts/helpers/GenericHarvester.sol @@ -82,7 +82,7 @@ contract GenericHarvester is BaseHarvester, IERC3156FlashBorrower, RouterSwapper address stablecoin, uint256 minAmountOut, SwapType swapType, - bytes calldata extraData + bytes memory extraData ) public virtual { flashloan.flashLoan( IERC3156FlashBorrower(address(this)), @@ -257,17 +257,14 @@ contract GenericHarvester is BaseHarvester, IERC3156FlashBorrower, RouterSwapper /// 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 `yieldBearingAsset` /// to the target exposure - function harvest( - address yieldBearingAsset, - uint256 scale, - SwapType swapType, - bytes calldata extraData - ) public virtual { + function harvest(address yieldBearingAsset, uint256 scale, bytes calldata extraData) public virtual { if (scale > 1e9) revert InvalidParam(); YieldBearingParams memory yieldBearingInfo = yieldBearingData[yieldBearingAsset]; (uint8 increase, uint256 amount) = _computeRebalanceAmount(yieldBearingAsset, yieldBearingInfo); amount = (amount * scale) / 1e9; + (SwapType swapType, bytes memory data) = abi.decode(extraData, (SwapType, bytes)); + if (amount > 0) { try transmuter.updateOracle(yieldBearingInfo.stablecoin) {} catch {} @@ -278,7 +275,7 @@ contract GenericHarvester is BaseHarvester, IERC3156FlashBorrower, RouterSwapper yieldBearingInfo.stablecoin, (amount * (1e9 - maxSlippage)) / 1e9, swapType, - extraData + data ); } } diff --git a/contracts/helpers/MultiBlockHarvester.sol b/contracts/helpers/MultiBlockHarvester.sol index add6ace8..26d288c8 100644 --- a/contracts/helpers/MultiBlockHarvester.sol +++ b/contracts/helpers/MultiBlockHarvester.sol @@ -91,7 +91,7 @@ contract MultiBlockHarvester is BaseHarvester { * @param scale scale to apply to the rebalance amount * @param yieldBearingAsset address of the yieldBearingAsset */ - function initiateRebalance(uint256 scale, address yieldBearingAsset) external onlyTrusted { + function harvest(address yieldBearingAsset, uint256 scale, bytes calldata) external onlyTrusted { if (scale > 1e9) revert InvalidParam(); YieldBearingParams memory yieldBearingInfo = yieldBearingData[yieldBearingAsset]; (uint8 increase, uint256 amount) = _computeRebalanceAmount(yieldBearingAsset, yieldBearingInfo); diff --git a/contracts/interfaces/IHarvester.sol b/contracts/interfaces/IHarvester.sol new file mode 100644 index 00000000..9deb028a --- /dev/null +++ b/contracts/interfaces/IHarvester.sol @@ -0,0 +1,28 @@ +// SPDX-License-Identifier: GPL-3.0 + +pragma solidity >=0.5.0; + +interface IHarvester { + function setYieldBearingAssetData( + address yieldBearingAsset, + uint64 targetExposure, + uint64 minExposureYieldAsset, + uint64 maxExposureYieldAsset, + uint64 overrideExposures + ) external; + + function setYieldBearingAssetData( + address yieldBearingAsset, + address stablecoin, + uint64 targetExposure, + uint64 minExposureYieldAsset, + uint64 maxExposureYieldAsset, + uint64 overrideExposures + ) external; + + function updateLimitExposuresYieldAsset(address yieldBearingAsset) external; + + function setMaxSlippage(uint96 newMaxSlippage) external; + + function harvest(address yieldBearingAsset, uint256 scale, bytes calldata extraData) external; +} From 6c3da100a62ae5a672287528a22f703b9a3e8312 Mon Sep 17 00:00:00 2001 From: 0xtekgrinder <0xtekgrinder@protonmail.com> Date: Mon, 23 Sep 2024 15:38:09 +0200 Subject: [PATCH 20/69] feat: checkSlippage with decimals --- contracts/helpers/MultiBlockHarvester.sol | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/contracts/helpers/MultiBlockHarvester.sol b/contracts/helpers/MultiBlockHarvester.sol index 26d288c8..511ce580 100644 --- a/contracts/helpers/MultiBlockHarvester.sol +++ b/contracts/helpers/MultiBlockHarvester.sol @@ -198,6 +198,14 @@ contract MultiBlockHarvester is BaseHarvester { //////////////////////////////////////////////////////////////////////////////////////////////////////////////////*/ function _checkSlippage(uint256 amountIn, uint256 amountOut, address asset, address depositAddress) internal view { + uint256 decimalsAsset = IERC20(asset).decimals(); + // Divide or multiply the amountIn to match the decimals of the asset + if (decimalsAsset > 18) { + amountIn /= 10 ** (decimalsAsset - 18); + } else if (decimalsAsset < 18) { + amountIn *= 10 ** (18 - decimalsAsset); + } + if (asset == USDC || asset == USDM) { // Assume 1:1 ratio between stablecoins uint256 slippage = ((amountIn - amountOut) * 1e9) / amountIn; From 5454e280f46d8d8276285441d9684ad1715158a9 Mon Sep 17 00:00:00 2001 From: 0xtekgrinder <0xtekgrinder@protonmail.com> Date: Mon, 23 Sep 2024 15:41:07 +0200 Subject: [PATCH 21/69] chore: remove old rebalancer interfaces --- contracts/helpers/MultiBlockHarvester.sol | 3 +- contracts/interfaces/IRebalancer.sol | 59 ------------------- contracts/interfaces/IRebalancerFlashloan.sol | 24 -------- 3 files changed, 2 insertions(+), 84 deletions(-) delete mode 100644 contracts/interfaces/IRebalancer.sol delete mode 100644 contracts/interfaces/IRebalancerFlashloan.sol diff --git a/contracts/helpers/MultiBlockHarvester.sol b/contracts/helpers/MultiBlockHarvester.sol index 511ce580..03583644 100644 --- a/contracts/helpers/MultiBlockHarvester.sol +++ b/contracts/helpers/MultiBlockHarvester.sol @@ -2,6 +2,7 @@ pragma solidity ^0.8.23; +import { IERC20Metadata } from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol"; import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import { AccessControl, IAccessControlManager } from "../utils/AccessControl.sol"; import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; @@ -198,7 +199,7 @@ contract MultiBlockHarvester is BaseHarvester { //////////////////////////////////////////////////////////////////////////////////////////////////////////////////*/ function _checkSlippage(uint256 amountIn, uint256 amountOut, address asset, address depositAddress) internal view { - uint256 decimalsAsset = IERC20(asset).decimals(); + uint256 decimalsAsset = IERC20Metadata(asset).decimals(); // Divide or multiply the amountIn to match the decimals of the asset if (decimalsAsset > 18) { amountIn /= 10 ** (decimalsAsset - 18); diff --git a/contracts/interfaces/IRebalancer.sol b/contracts/interfaces/IRebalancer.sol deleted file mode 100644 index 77e1b56b..00000000 --- a/contracts/interfaces/IRebalancer.sol +++ /dev/null @@ -1,59 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0 - -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; - // Decimals of the `tokenIn` associated to the order - uint8 decimalsIn; - // Decimals of the `tokenOut` associated to the order - uint8 decimalsOut; - // Guaranteed exchange rate in `BASE_18` for the swaps between the `tokenIn` and `tokenOut` associated to - // the order. This rate is a minimum rate guaranteed up to when the `subsidyBudget` is fully consumed - uint128 guaranteedRate; -} - -/// @title IRebalancer -/// @author Angle Labs, Inc. -interface IRebalancer { - /// @notice Swaps `tokenIn` for `tokenOut` through an intermediary agToken mint from `tokenIn` and - /// burn to `tokenOut`. Eventually, this transaction may be sponsored and yield an amount of `tokenOut` - /// higher than what would be obtained through a mint and burn directly on the `transmuter` - /// @param amountIn Amount of `tokenIn` to bring for the rebalancing - /// @param amountOutMin Minimum amount of `tokenOut` that must be obtained from the swap - /// @param to Address to which `tokenOut` must be sent - /// @param deadline Timestamp before which this transaction must be included - /// @return amountOut Amount of outToken obtained - function swapExactInput( - uint256 amountIn, - uint256 amountOutMin, - address tokenIn, - address tokenOut, - address to, - uint256 deadline - ) external returns (uint256 amountOut); - - /// @notice Approximates how much a call to `swapExactInput` with the same parameters would yield in terms - /// of `amountOut` - function quoteIn(uint256 amountIn, address tokenIn, address tokenOut) external view returns (uint256 amountOut); - - /// @notice Helper to compute the minimum guaranteed amount out that would be obtained from a swap of `amountIn` - /// of `tokenIn` to `tokenOut` - function getGuaranteedAmountOut( - address tokenIn, - address tokenOut, - uint256 amountIn - ) external view returns (uint256); - - /// @notice Lets governance set an order to subsidize rebalances between `tokenIn` and `tokenOut` - function setOrder(address tokenIn, address tokenOut, uint256 subsidyBudget, uint256 guaranteedRate) external; - - /// @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 deleted file mode 100644 index 9d4e5fb7..00000000 --- a/contracts/interfaces/IRebalancerFlashloan.sol +++ /dev/null @@ -1,24 +0,0 @@ -// 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; -} From dfddaa347951fa2f4eedfd5aae19c8e29bad9875 Mon Sep 17 00:00:00 2001 From: 0xtekgrinder <0xtekgrinder@protonmail.com> Date: Mon, 23 Sep 2024 15:46:20 +0200 Subject: [PATCH 22/69] style: fix lint issues for the new contracts --- .solhint.json | 3 ++- contracts/helpers/GenericHarvester.sol | 14 +++++++------- contracts/helpers/MultiBlockHarvester.sol | 2 +- 3 files changed, 10 insertions(+), 9 deletions(-) diff --git a/.solhint.json b/.solhint.json index e1346902..05e55962 100644 --- a/.solhint.json +++ b/.solhint.json @@ -8,6 +8,7 @@ "error", 120 ], + "immutable-vars-naming": "off", "avoid-call-value": "warn", "avoid-low-level-calls": "off", "avoid-tx-origin": "warn", @@ -38,4 +39,4 @@ "explicit" ] } -} \ No newline at end of file +} diff --git a/contracts/helpers/GenericHarvester.sol b/contracts/helpers/GenericHarvester.sol index 6c11cd8f..9e17321f 100644 --- a/contracts/helpers/GenericHarvester.sol +++ b/contracts/helpers/GenericHarvester.sol @@ -11,7 +11,7 @@ import { ITransmuter } from "interfaces/ITransmuter.sol"; import { IAgToken } from "interfaces/IAgToken.sol"; import { RouterSwapper } from "utils/src/RouterSwapper.sol"; -import { AccessControl, IAccessControlManager } from "../utils/AccessControl.sol"; +import { IAccessControlManager } from "../utils/AccessControl.sol"; import "../utils/Constants.sol"; import "../utils/Errors.sol"; @@ -251,12 +251,12 @@ contract GenericHarvester is BaseHarvester, IERC3156FlashBorrower, RouterSwapper HARVEST //////////////////////////////////////////////////////////////////////////////////////////////////////////////////*/ - /// @notice Invests or divests from the yield asset associated to `yieldBearingAsset` based on the current exposure to this - /// yieldBearingAsset - /// @dev This transaction either reduces the exposure to `yieldBearingAsset` in the Transmuter or frees up some yieldBearingAsset - /// 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 `yieldBearingAsset` - /// to the target exposure + /// @notice Invests or divests from the yield asset associated to `yieldBearingAsset` based on the current exposure + /// to this yieldBearingAsset + /// @dev This transaction either reduces the exposure to `yieldBearingAsset` in the Transmuter or frees up + /// some yieldBearingAsset 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 + /// `yieldBearingAsset` to the target exposure function harvest(address yieldBearingAsset, uint256 scale, bytes calldata extraData) public virtual { if (scale > 1e9) revert InvalidParam(); YieldBearingParams memory yieldBearingInfo = yieldBearingData[yieldBearingAsset]; diff --git a/contracts/helpers/MultiBlockHarvester.sol b/contracts/helpers/MultiBlockHarvester.sol index 03583644..b0c1fca4 100644 --- a/contracts/helpers/MultiBlockHarvester.sol +++ b/contracts/helpers/MultiBlockHarvester.sol @@ -4,7 +4,7 @@ pragma solidity ^0.8.23; import { IERC20Metadata } from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol"; import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; -import { AccessControl, IAccessControlManager } from "../utils/AccessControl.sol"; +import { IAccessControlManager } from "../utils/AccessControl.sol"; import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import { BaseHarvester, YieldBearingParams } from "./BaseHarvester.sol"; import { ITransmuter } from "../interfaces/ITransmuter.sol"; From ef574fcca5722fa696c7357f4813ffba39a645b3 Mon Sep 17 00:00:00 2001 From: 0xtekgrinder <0xtekgrinder@protonmail.com> Date: Mon, 30 Sep 2024 00:28:02 +0200 Subject: [PATCH 23/69] feat: remove useless setYieldBearingAssetData function override --- contracts/helpers/BaseHarvester.sol | 26 -------------------------- contracts/interfaces/IHarvester.sol | 8 -------- 2 files changed, 34 deletions(-) diff --git a/contracts/helpers/BaseHarvester.sol b/contracts/helpers/BaseHarvester.sol index 3bf1d711..b522fa5f 100644 --- a/contracts/helpers/BaseHarvester.sol +++ b/contracts/helpers/BaseHarvester.sol @@ -64,32 +64,6 @@ abstract contract BaseHarvester is IHarvester, AccessControl { GUARDIAN FUNCTIONS //////////////////////////////////////////////////////////////////////////////////////////////////////////////////*/ - /** - * @notice Set the yieldBearingAsset data - * @param yieldBearingAsset address of the yieldBearingAsset - * @param targetExposure target exposure to the yieldBearingAsset asset used - * @param minExposureYieldAsset minimum exposure within the Transmuter to the asset - * @param maxExposureYieldAsset maximum exposure within the Transmuter to the asset - * @param overrideExposures 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 - */ - function setYieldBearingAssetData( - address yieldBearingAsset, - uint64 targetExposure, - uint64 minExposureYieldAsset, - uint64 maxExposureYieldAsset, - uint64 overrideExposures - ) external onlyGuardian { - _setYieldBearingAssetData( - yieldBearingAsset, - yieldBearingAsset, - targetExposure, - minExposureYieldAsset, - maxExposureYieldAsset, - overrideExposures - ); - } - /** * @notice Set the yieldBearingAsset data * @param yieldBearingAsset address of the yieldBearingAsset diff --git a/contracts/interfaces/IHarvester.sol b/contracts/interfaces/IHarvester.sol index 9deb028a..3cc01066 100644 --- a/contracts/interfaces/IHarvester.sol +++ b/contracts/interfaces/IHarvester.sol @@ -3,14 +3,6 @@ pragma solidity >=0.5.0; interface IHarvester { - function setYieldBearingAssetData( - address yieldBearingAsset, - uint64 targetExposure, - uint64 minExposureYieldAsset, - uint64 maxExposureYieldAsset, - uint64 overrideExposures - ) external; - function setYieldBearingAssetData( address yieldBearingAsset, address stablecoin, From 4dd5a4f11a46a0162a45674384f0021744efdb52 Mon Sep 17 00:00:00 2001 From: 0xtekgrinder <0xtekgrinder@protonmail.com> Date: Mon, 7 Oct 2024 09:45:54 +0200 Subject: [PATCH 24/69] feat: correct computation of the rebalancing --- contracts/helpers/BaseHarvester.sol | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/contracts/helpers/BaseHarvester.sol b/contracts/helpers/BaseHarvester.sol index b522fa5f..5703e63a 100644 --- a/contracts/helpers/BaseHarvester.sol +++ b/contracts/helpers/BaseHarvester.sol @@ -123,8 +123,7 @@ abstract contract BaseHarvester is IHarvester, AccessControl { (uint256 stablecoinsFromStablecoin, ) = transmuter.getIssuedByCollateral(yieldBearingInfo.stablecoin); uint256 targetExposureScaled = yieldBearingInfo.targetExposure * stablecoinsIssued; if (stablecoinsFromYieldBearingAsset * 1e9 > targetExposureScaled) { - // Need to increase exposure to yield bearing asset - increase = 1; + // Need to decrease exposure to yield bearing asset amount = stablecoinsFromYieldBearingAsset - targetExposureScaled / 1e9; uint256 maxValueScaled = yieldBearingInfo.maxExposureYieldAsset * stablecoinsIssued; // These checks assume that there are no transaction fees on the stablecoin->collateral conversion and so @@ -135,6 +134,7 @@ abstract contract BaseHarvester is IHarvester, AccessControl { } else { // In this case, exposure after the operation might remain slightly below the targetExposure as less // collateral may be obtained by burning stablecoins for the yield asset and unwrapping it + increase = 1; amount = targetExposureScaled / 1e9 - stablecoinsFromYieldBearingAsset; uint256 minValueScaled = yieldBearingInfo.minExposureYieldAsset * stablecoinsIssued; if (stablecoinsFromStablecoin * 1e9 < minValueScaled) amount = 0; From d8c9aa11a508454d67218d0248064d4130adf677 Mon Sep 17 00:00:00 2001 From: 0xtekgrinder <0xtekgrinder@protonmail.com> Date: Mon, 7 Oct 2024 09:46:36 +0200 Subject: [PATCH 25/69] fix: correct sleeping check + toggleTrusted --- contracts/helpers/MultiBlockHarvester.sol | 42 ++++++++++++++++++----- 1 file changed, 33 insertions(+), 9 deletions(-) diff --git a/contracts/helpers/MultiBlockHarvester.sol b/contracts/helpers/MultiBlockHarvester.sol index b0c1fca4..d3ffe83e 100644 --- a/contracts/helpers/MultiBlockHarvester.sol +++ b/contracts/helpers/MultiBlockHarvester.sol @@ -83,6 +83,14 @@ contract MultiBlockHarvester is BaseHarvester { yieldBearingToDepositAddress[yieldBearingAsset] = newDepositAddress; } + /** + * @notice Toggle the trusted status of an address + * @param trusted address to toggle the trusted status + */ + function toggleTrusted(address trusted) external onlyGuardian { + isTrusted[trusted] = !isTrusted[trusted]; + } + /*////////////////////////////////////////////////////////////////////////////////////////////////////////////////// TRUSTED FUNCTIONS //////////////////////////////////////////////////////////////////////////////////////////////////////////////////*/ @@ -97,6 +105,7 @@ contract MultiBlockHarvester is BaseHarvester { YieldBearingParams memory yieldBearingInfo = yieldBearingData[yieldBearingAsset]; (uint8 increase, uint256 amount) = _computeRebalanceAmount(yieldBearingAsset, yieldBearingInfo); amount = (amount * scale) / 1e9; + if (amount == 0) revert ZeroAmount(); try transmuter.updateOracle(yieldBearingAsset) {} catch {} _rebalance(increase, yieldBearingAsset, yieldBearingInfo, amount); @@ -108,7 +117,7 @@ contract MultiBlockHarvester is BaseHarvester { */ function finalizeRebalance(address yieldBearingAsset, uint256 balance) external onlyTrusted { try transmuter.updateOracle(yieldBearingAsset) {} catch {} - _adjustAllowance(address(agToken), address(transmuter), balance); + _adjustAllowance(yieldBearingAsset, address(transmuter), balance); uint256 amountOut = transmuter.swapExactInput( balance, 0, @@ -120,7 +129,7 @@ contract MultiBlockHarvester is BaseHarvester { address depositAddress = yieldBearingAsset == XEVT ? yieldBearingToDepositAddress[yieldBearingAsset] : address(0); - _checkSlippage(balance, amountOut, yieldBearingAsset, depositAddress); + _checkSlippage(balance, amountOut, yieldBearingAsset, depositAddress, true); agToken.burnSelf(amountOut, address(this)); } @@ -149,7 +158,7 @@ contract MultiBlockHarvester is BaseHarvester { address(this), block.timestamp ); - _checkSlippage(amount, amountOut, yieldBearingInfo.stablecoin, depositAddress); + _checkSlippage(amount, amountOut, yieldBearingInfo.stablecoin, depositAddress, false); _adjustAllowance(yieldBearingInfo.stablecoin, address(depositAddress), amountOut); (uint256 shares, ) = IPool(depositAddress).deposit(amountOut, address(this)); _adjustAllowance(yieldBearingAsset, address(transmuter), shares); @@ -171,7 +180,7 @@ contract MultiBlockHarvester is BaseHarvester { address(this), block.timestamp ); - _checkSlippage(amount, amountOut, yieldBearingInfo.stablecoin, depositAddress); + _checkSlippage(amount, amountOut, yieldBearingInfo.stablecoin, depositAddress, false); IERC20(yieldBearingInfo.stablecoin).safeTransfer(depositAddress, amountOut); } } else { @@ -184,7 +193,7 @@ contract MultiBlockHarvester is BaseHarvester { block.timestamp ); address depositAddress = yieldBearingToDepositAddress[yieldBearingAsset]; - _checkSlippage(amount, amountOut, yieldBearingAsset, depositAddress); + _checkSlippage(amount, amountOut, yieldBearingAsset, depositAddress, false); if (yieldBearingAsset == XEVT) { IPool(depositAddress).requestRedeem(amountOut); @@ -198,16 +207,31 @@ contract MultiBlockHarvester is BaseHarvester { HELPER //////////////////////////////////////////////////////////////////////////////////////////////////////////////////*/ - function _checkSlippage(uint256 amountIn, uint256 amountOut, address asset, address depositAddress) internal view { + function _checkSlippage( + uint256 amountIn, + uint256 amountOut, + address asset, + address depositAddress, + bool assetIn + ) internal view { uint256 decimalsAsset = IERC20Metadata(asset).decimals(); + // Divide or multiply the amountIn to match the decimals of the asset if (decimalsAsset > 18) { - amountIn /= 10 ** (decimalsAsset - 18); + if (assetIn) { + amountIn /= 10 ** (decimalsAsset - 18); + } else { + amountIn *= 10 ** (decimalsAsset - 18); + } } else if (decimalsAsset < 18) { - amountIn *= 10 ** (18 - decimalsAsset); + if (assetIn) { + amountIn *= 10 ** (18 - decimalsAsset); + } else { + amountIn /= 10 ** (18 - decimalsAsset); + } } - if (asset == USDC || asset == USDM) { + if (asset == USDC || asset == USDM || asset == EURC) { // Assume 1:1 ratio between stablecoins uint256 slippage = ((amountIn - amountOut) * 1e9) / amountIn; if (slippage > maxSlippage) revert SlippageTooHigh(); From 3eb3da799a741ff21cd48e15eb11865d0e491b24 Mon Sep 17 00:00:00 2001 From: 0xtekgrinder <0xtekgrinder@protonmail.com> Date: Tue, 8 Oct 2024 08:33:29 +0200 Subject: [PATCH 26/69] TEMP feat: tests --- test/fuzz/GenericHarvester.t.sol | 0 test/fuzz/Harvester.t.sol | 4 + test/fuzz/MultiBlockHarvester.t.sol | 417 ++++++++++++ test/fuzz/Rebalancer.t.sol | 653 ------------------- test/fuzz/RebalancerFlashloanVaultTest.t.sol | 89 --- test/scripts/HarvesterSwapUSDATest.t.sol | 136 ---- test/scripts/HarvesterUSDATest.t.sol | 237 ------- test/scripts/RebalancerSwapUSDATest.t.sol | 452 ------------- test/scripts/RebalancerUSDATest.t.sol | 322 --------- 9 files changed, 421 insertions(+), 1889 deletions(-) create mode 100644 test/fuzz/GenericHarvester.t.sol create mode 100644 test/fuzz/MultiBlockHarvester.t.sol delete mode 100644 test/fuzz/Rebalancer.t.sol delete mode 100644 test/fuzz/RebalancerFlashloanVaultTest.t.sol delete mode 100644 test/scripts/HarvesterSwapUSDATest.t.sol delete mode 100644 test/scripts/HarvesterUSDATest.t.sol delete mode 100644 test/scripts/RebalancerSwapUSDATest.t.sol delete mode 100644 test/scripts/RebalancerUSDATest.t.sol diff --git a/test/fuzz/GenericHarvester.t.sol b/test/fuzz/GenericHarvester.t.sol new file mode 100644 index 00000000..e69de29b diff --git a/test/fuzz/Harvester.t.sol b/test/fuzz/Harvester.t.sol index 5bcb2e71..f17468e2 100644 --- a/test/fuzz/Harvester.t.sol +++ b/test/fuzz/Harvester.t.sol @@ -1,6 +1,8 @@ // 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"; @@ -250,3 +252,5 @@ contract HarvesterTest is Fixture, FunctionUtils { vm.stopPrank(); } } + +*/ diff --git a/test/fuzz/MultiBlockHarvester.t.sol b/test/fuzz/MultiBlockHarvester.t.sol new file mode 100644 index 00000000..4ebf18b1 --- /dev/null +++ b/test/fuzz/MultiBlockHarvester.t.sol @@ -0,0 +1,417 @@ +// 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/MultiBlockHarvester.sol"; + +import "contracts/transmuter/Storage.sol"; + +import { IERC4626 } from "oz/token/ERC20/extensions/ERC4626.sol"; +import { IAccessControl } from "oz/access/IAccessControl.sol"; + +contract MultiBlockHarvestertTest is Fixture, FunctionUtils { + using SafeERC20 for IERC20; + + MultiBlockHarvester public harvester; + uint64 public targetExposure; + uint64 public maxExposureYieldAsset; + uint64 public minExposureYieldAsset; + + AggregatorV3Interface public oracleUSDC; + AggregatorV3Interface public oracleEURC; + AggregatorV3Interface public oracleXEVT; + AggregatorV3Interface public oracleUSDM; + + address public receiver; + + function setUp() public override { + super.setUp(); + + receiver = makeAddr("receiver"); + + vm.createSelectFork("mainnet"); + + // set mint Fees to 0 on all collaterals + uint64[] memory xFeeMint = new uint64[](1); + xFeeMint[0] = uint64(0); + uint64[] memory xFeeBurn = new uint64[](1); + xFeeBurn[0] = uint64(BASE_9); + int64[] memory yFeeMint = new int64[](1); + yFeeMint[0] = 0; + int64[] memory yFeeBurn = new int64[](1); + yFeeBurn[0] = 0; + int64[] memory yFeeRedemption = new int64[](1); + yFeeRedemption[0] = int64(int256(BASE_9)); + vm.startPrank(governor); + + // remove fixture collateral + transmuter.revokeCollateral(address(eurA)); + transmuter.revokeCollateral(address(eurB)); + transmuter.revokeCollateral(address(eurY)); + + transmuter.addCollateral(XEVT); + transmuter.addCollateral(USDM); + transmuter.addCollateral(EURC); + transmuter.addCollateral(USDC); + + AggregatorV3Interface[] memory circuitChainlink = new AggregatorV3Interface[](1); + uint32[] memory stalePeriods = new uint32[](1); + uint8[] memory circuitChainIsMultiplied = new uint8[](1); + uint8[] memory chainlinkDecimals = new uint8[](1); + stalePeriods[0] = 1 hours; + circuitChainIsMultiplied[0] = 1; + chainlinkDecimals[0] = 8; + OracleQuoteType quoteType = OracleQuoteType.UNIT; + bytes memory targetData; + bytes memory readData; + oracleEURC = AggregatorV3Interface(address(new MockChainlinkOracle())); + circuitChainlink[0] = AggregatorV3Interface(oracleEURC); + readData = abi.encode(circuitChainlink, stalePeriods, circuitChainIsMultiplied, chainlinkDecimals, quoteType); + MockChainlinkOracle(address(oracleEURC)).setLatestAnswer(int256(BASE_8)); + transmuter.setOracle( + EURC, + abi.encode( + OracleReadType.CHAINLINK_FEEDS, + OracleReadType.STABLE, + readData, + targetData, + abi.encode(uint128(0), uint128(0)) + ) + ); + + oracleUSDC = AggregatorV3Interface(address(new MockChainlinkOracle())); + circuitChainlink[0] = AggregatorV3Interface(oracleUSDC); + readData = abi.encode(circuitChainlink, stalePeriods, circuitChainIsMultiplied, chainlinkDecimals, quoteType); + MockChainlinkOracle(address(oracleUSDC)).setLatestAnswer(int256(BASE_8)); + transmuter.setOracle( + USDC, + abi.encode( + OracleReadType.CHAINLINK_FEEDS, + OracleReadType.STABLE, + readData, + targetData, + abi.encode(uint128(0), uint128(0)) + ) + ); + + oracleXEVT = AggregatorV3Interface(address(new MockChainlinkOracle())); + circuitChainlink[0] = AggregatorV3Interface(oracleXEVT); + readData = abi.encode(circuitChainlink, stalePeriods, circuitChainIsMultiplied, chainlinkDecimals, quoteType); + MockChainlinkOracle(address(oracleXEVT)).setLatestAnswer(int256(IERC4626(XEVT).convertToAssets(BASE_8))); + transmuter.setOracle( + XEVT, + abi.encode( + OracleReadType.CHAINLINK_FEEDS, + OracleReadType.STABLE, + readData, + targetData, + abi.encode(uint128(0), uint128(0)) + ) + ); + + oracleUSDM = AggregatorV3Interface(address(new MockChainlinkOracle())); + circuitChainlink[0] = AggregatorV3Interface(oracleUSDM); + readData = abi.encode(circuitChainlink, stalePeriods, circuitChainIsMultiplied, chainlinkDecimals, quoteType); + MockChainlinkOracle(address(oracleUSDM)).setLatestAnswer(int256(BASE_8)); + transmuter.setOracle( + USDM, + abi.encode( + OracleReadType.CHAINLINK_FEEDS, + OracleReadType.STABLE, + readData, + targetData, + abi.encode(uint128(0), uint128(0)) + ) + ); + + transmuter.togglePause(XEVT, ActionType.Mint); + transmuter.togglePause(XEVT, ActionType.Burn); + transmuter.setStablecoinCap(XEVT, type(uint256).max); + transmuter.togglePause(EURC, ActionType.Mint); + transmuter.togglePause(EURC, ActionType.Burn); + transmuter.setStablecoinCap(EURC, type(uint256).max); + transmuter.togglePause(USDM, ActionType.Mint); + transmuter.togglePause(USDM, ActionType.Burn); + transmuter.setStablecoinCap(USDM, type(uint256).max); + transmuter.togglePause(USDC, ActionType.Mint); + transmuter.togglePause(USDC, ActionType.Burn); + transmuter.setStablecoinCap(USDC, type(uint256).max); + + // mock isAllowed(address) returns (bool) + vm.mockCall( + 0x9019Fd383E490B4B045130707C9A1227F36F4636, + abi.encodeWithSelector(Wow.isAllowed.selector), + abi.encode(true) + ); + + transmuter.setFees(address(XEVT), xFeeMint, yFeeMint, true); + transmuter.setFees(address(XEVT), xFeeBurn, yFeeBurn, false); + transmuter.setFees(address(USDM), xFeeMint, yFeeMint, true); + transmuter.setFees(address(USDM), xFeeBurn, yFeeBurn, false); + transmuter.setFees(address(EURC), xFeeMint, yFeeMint, true); + transmuter.setFees(address(EURC), xFeeBurn, yFeeBurn, false); + transmuter.setFees(address(USDC), xFeeMint, yFeeMint, true); + transmuter.setFees(address(USDC), xFeeBurn, yFeeBurn, false); + transmuter.setRedemptionCurveParams(xFeeMint, yFeeRedemption); + vm.stopPrank(); + + targetExposure = uint64((15 * 1e9) / 100); + maxExposureYieldAsset = uint64((80 * 1e9) / 100); + minExposureYieldAsset = uint64((5 * 1e9) / 100); + + harvester = new MultiBlockHarvester(100_000e18, 1e8, accessControlManager, agToken, transmuter); + vm.startPrank(governor); + harvester.toggleTrusted(alice); + harvester.setYieldBearingToDepositAddress(XEVT, XEVT); + harvester.setYieldBearingToDepositAddress(USDM, receiver); + + transmuter.toggleTrusted(address(harvester), TrustedType.Seller); + + vm.stopPrank(); + + vm.label(XEVT, "XEVT"); + vm.label(USDM, "USDM"); + vm.label(EURC, "EURC"); + vm.label(USDC, "USDC"); + vm.label(address(harvester), "Harvester"); + } + + function test_Initialization() public { + assertEq(harvester.maxSlippage(), 1e8); + assertEq(harvester.maxMintAmount(), 100_000e18); + assertEq(address(harvester.accessControlManager()), address(accessControlManager)); + assertEq(address(harvester.agToken()), address(agToken)); + assertEq(address(harvester.transmuter()), address(transmuter)); + } + + function test_OnlyGuardian_RevertWhen_NotGuardian() public { + vm.expectRevert(Errors.NotGovernorOrGuardian.selector); + harvester.setYieldBearingAssetData( + address(XEVT), + address(EURC), + targetExposure, + 1, + maxExposureYieldAsset, + minExposureYieldAsset + ); + + vm.expectRevert(Errors.NotGovernorOrGuardian.selector); + harvester.setMaxSlippage(1e9); + + vm.expectRevert(Errors.NotGovernorOrGuardian.selector); + harvester.updateLimitExposuresYieldAsset(address(XEVT)); + } + + function test_SettersHarvester() public { + vm.startPrank(governor); + vm.expectRevert(Errors.InvalidParam.selector); + harvester.setMaxSlippage(1e10); + + harvester.setMaxSlippage(123456); + assertEq(harvester.maxSlippage(), 123456); + + harvester.setYieldBearingAssetData( + address(XEVT), + address(EURC), + targetExposure + 10, + minExposureYieldAsset - 1, + maxExposureYieldAsset + 1, + 1 + ); + (address stablecoin, uint64 target, uint64 maxi, uint64 mini, uint64 overrideExp) = harvester.yieldBearingData( + address(XEVT) + ); + assertEq(stablecoin, address(EURC)); + assertEq(target, targetExposure + 10); + assertEq(maxi, maxExposureYieldAsset + 1); + assertEq(mini, minExposureYieldAsset - 1); + assertEq(overrideExp, 1); + + harvester.setYieldBearingAssetData( + address(XEVT), + address(EURC), + targetExposure + 10, + minExposureYieldAsset - 1, + maxExposureYieldAsset + 1, + 0 + ); + (stablecoin, target, maxi, mini, overrideExp) = harvester.yieldBearingData(address(XEVT)); + assertEq(stablecoin, address(EURC)); + assertEq(target, targetExposure + 10); + assertEq(maxi, 1e9); + assertEq(mini, 0); + assertEq(overrideExp, 2); + + vm.stopPrank(); + } + + function test_UpdateLimitExposuresYieldAsset() public { + bytes memory data; + address _savingImplementation = address(new Savings()); + Savings newVault = Savings(_deployUpgradeable(address(proxyAdmin), _savingImplementation, data)); + string memory _name = "savingAgEUR"; + string memory _symbol = "SAGEUR"; + + vm.startPrank(governor); + MockTokenPermit(address(eurA)).mint(governor, 1e12); + eurA.approve(address(newVault), 1e12); + newVault.initialize(accessControlManager, IERC20MetadataUpgradeable(address(eurA)), _name, _symbol, BASE_6); + transmuter.addCollateral(address(newVault)); + vm.stopPrank(); + + uint64[] memory xFeeMint = new uint64[](3); + int64[] memory yFeeMint = new int64[](3); + + xFeeMint[0] = 0; + xFeeMint[1] = uint64((15 * BASE_9) / 100); + xFeeMint[2] = uint64((2 * BASE_9) / 10); + + yFeeMint[0] = int64(1); + yFeeMint[1] = int64(uint64(BASE_9 / 10)); + yFeeMint[2] = int64(uint64((2 * BASE_9) / 10)); + + uint64[] memory xFeeBurn = new uint64[](3); + int64[] memory yFeeBurn = new int64[](3); + + xFeeBurn[0] = uint64(BASE_9); + xFeeBurn[1] = uint64(BASE_9 / 10); + xFeeBurn[2] = 0; + + yFeeBurn[0] = int64(1); + yFeeBurn[1] = int64(1); + yFeeBurn[2] = int64(uint64(BASE_9 / 10)); + + vm.startPrank(governor); + transmuter.setFees(address(newVault), xFeeBurn, yFeeBurn, false); + transmuter.setFees(address(newVault), xFeeMint, yFeeMint, true); + harvester.setYieldBearingAssetData( + address(newVault), + address(eurA), + targetExposure, + minExposureYieldAsset, + maxExposureYieldAsset, + 0 + ); + harvester.updateLimitExposuresYieldAsset(address(newVault)); + + (, , uint64 maxi, uint64 mini, ) = harvester.yieldBearingData(address(newVault)); + assertEq(maxi, (15 * BASE_9) / 100); + assertEq(mini, BASE_9 / 10); + vm.stopPrank(); + } + + function test_FinalizeRebalance_IncreaseExposureXEVT(uint256 amount) external { + _loadReserve(XEVT, 1e26); + amount = bound(amount, 1e18, 1e24); + deal(XEVT, address(harvester), amount); + + vm.prank(alice); + harvester.finalizeRebalance(XEVT, amount); + + assertEq(agToken.balanceOf(address(harvester)), 0); + assertEq(IERC20(XEVT).balanceOf(address(harvester)), 0); + } + + function test_FinalizeRebalance_DecreaseExposureEURC(uint256 amount) external { + _loadReserve(EURC, 1e26); + amount = bound(amount, 1e18, 1e24); + deal(EURC, address(harvester), amount); + + vm.prank(alice); + harvester.finalizeRebalance(EURC, amount); + + assertEq(agToken.balanceOf(address(harvester)), 0); + assertEq(IERC20(EURC).balanceOf(address(harvester)), 0); + } + + function test_harvest_TooBigMintedAmount() external { + _loadReserve(EURC, 1e26); + _loadReserve(XEVT, 1e6); + _setYieldBearingData(XEVT, EURC); + + vm.expectRevert(TooBigAmountIn.selector); + vm.prank(alice); + harvester.harvest(XEVT, 1e9, new bytes(0)); + } + + function test_harvest_IncreaseExposureXEVT(uint256 amount) external { + _loadReserve(EURC, 1e11); + _loadReserve(XEVT, 1e6); + _setYieldBearingData(XEVT, EURC); + + vm.prank(alice); + harvester.harvest(XEVT, 1e9, new bytes(0)); + } + + function test_harvest_DecreaseExposureXEVT(uint256 amount) external { + _loadReserve(EURC, 1e11); + _loadReserve(XEVT, 1e8); + _setYieldBearingData(XEVT, EURC); + + vm.prank(alice); + harvester.harvest(XEVT, 1e9, new bytes(0)); + } + + function test_harvest_IncreaseExposureUSDM(uint256 amount) external { + _loadReserve(USDC, 1e11); + _loadReserve(USDM, 1e6); + _setYieldBearingData(USDM, USDC); + + vm.prank(alice); + harvester.harvest(USDM, 1e9, new bytes(0)); + } + + function test_harvest_DecreaseExposureUSDM(uint256 amount) external { + _loadReserve(USDC, 1e11); + _loadReserve(USDM, 1e8); + _setYieldBearingData(USDM, USDC); + + vm.prank(alice); + harvester.harvest(USDM, 1e9, new bytes(0)); + } + + function test_FinalizeRebalance_SlippageTooHigh(uint256 amount) external { + // TODO + } + + function _loadReserve(address token, uint256 amount) internal { + if (token == USDM) { + vm.prank(0x48AEB395FB0E4ff8433e9f2fa6E0579838d33B62); + IAgToken(USDM).mint(alice, amount); + } else { + deal(token, alice, amount); + } + + vm.startPrank(alice); + IERC20(token).approve(address(transmuter), type(uint256).max); + transmuter.swapExactInput(amount, 0, token, address(agToken), alice, block.timestamp + 1); + vm.stopPrank(); + } + + function _setYieldBearingData(address yieldBearingAsset, address stablecoin) internal { + vm.prank(governor); + harvester.setYieldBearingAssetData( + yieldBearingAsset, + stablecoin, + targetExposure, + minExposureYieldAsset, + maxExposureYieldAsset, + 1 + ); + } +} + +interface Wow { + function isAllowed(address) external returns (bool); +} diff --git a/test/fuzz/Rebalancer.t.sol b/test/fuzz/Rebalancer.t.sol deleted file mode 100644 index b9480ba8..00000000 --- a/test/fuzz/Rebalancer.t.sol +++ /dev/null @@ -1,653 +0,0 @@ -// 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/helpers/Rebalancer.sol"; - -contract RebalancerTest is Fixture, FunctionUtils { - using SafeERC20 for IERC20; - - address[] internal _collaterals; - - Rebalancer public rebalancer; - uint256 public decimalsA; - uint256 public decimalsB; - uint256 public decimalsY; - - function setUp() public override { - super.setUp(); - - // set mint Fees to 0 on all collaterals - uint64[] memory xFeeMint = new uint64[](1); - xFeeMint[0] = uint64(0); - uint64[] memory xFeeBurn = new uint64[](1); - xFeeBurn[0] = uint64(BASE_9); - int64[] memory yFeeMint = new int64[](1); - yFeeMint[0] = 0; - int64[] memory yFeeBurn = new int64[](1); - yFeeBurn[0] = 0; - int64[] memory yFeeRedemption = new int64[](1); - yFeeRedemption[0] = int64(int256(BASE_9)); - vm.startPrank(governor); - transmuter.setFees(address(eurA), xFeeMint, yFeeMint, true); - transmuter.setFees(address(eurA), xFeeBurn, yFeeBurn, false); - transmuter.setFees(address(eurB), xFeeMint, yFeeMint, true); - transmuter.setFees(address(eurB), xFeeBurn, yFeeBurn, false); - transmuter.setFees(address(eurY), xFeeMint, yFeeMint, true); - transmuter.setFees(address(eurY), xFeeBurn, yFeeBurn, false); - transmuter.setRedemptionCurveParams(xFeeMint, yFeeRedemption); - vm.stopPrank(); - - _collaterals.push(address(eurA)); - _collaterals.push(address(eurB)); - _collaterals.push(address(eurY)); - rebalancer = new Rebalancer(accessControlManager, transmuter); - - decimalsA = 10 ** IERC20Metadata(address(eurA)).decimals(); - decimalsB = 10 ** IERC20Metadata(address(eurB)).decimals(); - decimalsY = 10 ** IERC20Metadata(address(eurY)).decimals(); - } - - /*////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - INITIALIZATION - //////////////////////////////////////////////////////////////////////////////////////////////////////////////////*/ - - function test_RebalancerInitialization() public { - assertEq(address(rebalancer.accessControlManager()), address(accessControlManager)); - assertEq(address(rebalancer.AGTOKEN()), address(agToken)); - assertEq(address(rebalancer.TRANSMUTER()), address(transmuter)); - } - - function test_Constructor_RevertWhen_ZeroAddress() public { - vm.expectRevert(Errors.ZeroAddress.selector); - new Rebalancer(IAccessControlManager(address(0)), transmuter); - - vm.expectRevert(); - new Rebalancer(accessControlManager, ITransmuter(address(0))); - } - - /*////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - SET ORDER - //////////////////////////////////////////////////////////////////////////////////////////////////////////////////*/ - - function test_SetOrder_RevertWhen_NonGovernorOrGuardian() public { - vm.expectRevert(Errors.NotGovernorOrGuardian.selector); - rebalancer.setOrder(address(eurA), address(eurB), 100, 1); - } - - function test_SetOrder_RevertWhen_InvalidParam() public { - vm.startPrank(governor); - vm.expectRevert(Errors.InvalidParam.selector); - rebalancer.setOrder(address(eurA), address(eurB), 100, 1); - vm.stopPrank(); - } - - function test_SetOrder_RevertWhen_NotCollateral() public { - vm.startPrank(governor); - vm.expectRevert(Errors.NotCollateral.selector); - rebalancer.setOrder(address(eurA), address(agToken), 100, 1); - - vm.expectRevert(Errors.NotCollateral.selector); - rebalancer.setOrder(address(agToken), address(eurB), 100, 1); - - vm.expectRevert(Errors.NotCollateral.selector); - rebalancer.setOrder(address(agToken), address(agToken), 100, 1); - vm.stopPrank(); - } - - function testFuzz_SetOrder( - uint256 subsidyBudget, - uint256 guaranteedRate, - uint256 subsidyBudget1, - uint256 guaranteedRate1 - ) public { - uint256 a; - uint256 b; - subsidyBudget = bound(subsidyBudget, 10 ** 9, 10 ** 27); - guaranteedRate = bound(guaranteedRate, 10 ** 15, 10 ** 21); - subsidyBudget1 = bound(subsidyBudget1, 10 ** 9, 10 ** 27); - guaranteedRate1 = bound(guaranteedRate1, 10 ** 15, 10 ** 21); - deal(address(agToken), address(rebalancer), subsidyBudget); - vm.startPrank(governor); - rebalancer.setOrder(address(eurA), address(eurB), subsidyBudget, guaranteedRate); - vm.stopPrank(); - uint256 _decimalsA; - uint256 _decimalsB; - (a, _decimalsA, _decimalsB, b) = rebalancer.orders(address(eurA), address(eurB)); - assertEq(10 ** _decimalsA, decimalsA); - assertEq(10 ** _decimalsB, decimalsB); - assertEq(a, subsidyBudget); - assertEq(b, guaranteedRate); - assertEq(rebalancer.budget(), subsidyBudget); - assertEq( - rebalancer.getGuaranteedAmountOut(address(eurA), address(eurB), decimalsA), - (guaranteedRate * decimalsB) / 1e18 - ); - - vm.startPrank(governor); - vm.expectRevert(Errors.InvalidParam.selector); - rebalancer.setOrder(address(eurA), address(eurB), subsidyBudget + 1, guaranteedRate + 1); - vm.stopPrank(); - - vm.startPrank(governor); - rebalancer.setOrder(address(eurA), address(eurB), subsidyBudget / 3, guaranteedRate); - vm.stopPrank(); - (a, , , b) = rebalancer.orders(address(eurA), address(eurB)); - assertEq(a, subsidyBudget / 3); - assertEq(b, guaranteedRate); - assertEq(rebalancer.budget(), subsidyBudget / 3); - - assertEq( - rebalancer.getGuaranteedAmountOut(address(eurA), address(eurB), decimalsA), - (guaranteedRate * decimalsB) / 1e18 - ); - vm.startPrank(governor); - rebalancer.setOrder(address(eurA), address(eurB), subsidyBudget, guaranteedRate / 2); - vm.stopPrank(); - (a, , , b) = rebalancer.orders(address(eurA), address(eurB)); - assertEq(a, subsidyBudget); - assertEq(b, guaranteedRate / 2); - assertEq(rebalancer.budget(), subsidyBudget); - assertEq( - rebalancer.getGuaranteedAmountOut(address(eurA), address(eurB), decimalsA), - ((guaranteedRate / 2) * decimalsB) / 1e18 - ); - // Resetting to normal - vm.startPrank(governor); - rebalancer.setOrder(address(eurA), address(eurB), subsidyBudget, guaranteedRate); - vm.stopPrank(); - - // Now checking with multi token budget - - vm.startPrank(governor); - if (guaranteedRate1 > 0) { - vm.expectRevert(Errors.InvalidParam.selector); - rebalancer.setOrder(address(eurY), address(eurB), subsidyBudget1, guaranteedRate1); - } - deal(address(agToken), address(rebalancer), subsidyBudget1 + subsidyBudget); - - rebalancer.setOrder(address(eurY), address(eurB), subsidyBudget1, guaranteedRate1); - vm.stopPrank(); - (a, , , b) = rebalancer.orders(address(eurY), address(eurB)); - assertEq(a, subsidyBudget1); - assertEq(b, guaranteedRate1); - assertEq(rebalancer.budget(), subsidyBudget + subsidyBudget1); - assertEq( - rebalancer.getGuaranteedAmountOut(address(eurY), address(eurB), decimalsY), - (guaranteedRate1 * decimalsB) / 1e18 - ); - vm.startPrank(governor); - if (guaranteedRate1 > 0) { - vm.expectRevert(Errors.InvalidParam.selector); - rebalancer.setOrder(address(eurY), address(eurB), subsidyBudget1 + 1, guaranteedRate1 + 1); - } - - rebalancer.setOrder(address(eurY), address(eurB), subsidyBudget1, guaranteedRate1 / 2); - vm.stopPrank(); - (a, , , b) = rebalancer.orders(address(eurY), address(eurB)); - assertEq(a, subsidyBudget1); - assertEq(b, guaranteedRate1 / 2); - assertEq(rebalancer.budget(), subsidyBudget + subsidyBudget1); - assertEq( - rebalancer.getGuaranteedAmountOut(address(eurY), address(eurB), decimalsY), - ((guaranteedRate1 / 2) * decimalsB) / 1e18 - ); - - vm.startPrank(governor); - rebalancer.setOrder(address(eurY), address(eurB), subsidyBudget1 / 3, guaranteedRate1); - vm.stopPrank(); - (a, , , b) = rebalancer.orders(address(eurY), address(eurB)); - assertEq(a, subsidyBudget1 / 3); - assertEq(b, guaranteedRate1); - assertEq(rebalancer.budget(), subsidyBudget + subsidyBudget1 / 3); - assertEq( - rebalancer.getGuaranteedAmountOut(address(eurY), address(eurB), decimalsY), - (guaranteedRate1 * decimalsB) / 1e18 - ); - } - - /*////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - RECOVER - //////////////////////////////////////////////////////////////////////////////////////////////////////////////////*/ - - function test_Recover_RevertWhen_NonGovernorOrGuardian() public { - vm.expectRevert(Errors.NotGovernorOrGuardian.selector); - rebalancer.recover(address(eurA), 100, address(governor)); - } - - function test_Recover_RevertWhen_InvalidParam() public { - vm.startPrank(governor); - vm.expectRevert(); - rebalancer.recover(address(eurA), 100, address(governor)); - vm.expectRevert(Errors.InvalidParam.selector); - rebalancer.recover(address(agToken), 100, address(governor)); - vm.stopPrank(); - } - - function testFuzz_Recover(uint256 amount) public { - amount = bound(amount, 10 ** 9, 10 ** 27); - deal(address(eurB), address(rebalancer), amount); - vm.startPrank(governor); - rebalancer.recover(address(eurB), amount / 2, address(governor)); - vm.stopPrank(); - assertEq(eurB.balanceOf(address(governor)), amount / 2); - vm.startPrank(governor); - deal(address(agToken), address(rebalancer), amount); - rebalancer.setOrder(address(eurA), address(eurB), amount / 2, BASE_18 / 100); - vm.stopPrank(); - (uint256 a, , , uint256 b) = rebalancer.orders(address(eurA), address(eurB)); - assertEq(a, amount / 2); - assertEq(b, BASE_18 / 100); - vm.startPrank(governor); - vm.expectRevert(Errors.InvalidParam.selector); - rebalancer.recover(address(agToken), amount - 1, address(governor)); - vm.stopPrank(); - } - - /*////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - QUOTE IN - //////////////////////////////////////////////////////////////////////////////////////////////////////////////////*/ - - function testFuzz_QuoteInWithoutSubsidy(uint256 multiplier, uint256 swapMultiplier) public { - multiplier = bound(multiplier, 10, 10 ** 6); - swapMultiplier = bound(swapMultiplier, 1, 100); - // let's first load the reserves of the protocol - _loadReserves(charlie, multiplier); - // Here there are no fees, and oracles are all constant -> so any mint and burn should be an onpar - // conversion for both tokens - - uint256 swapAmount = (multiplier * swapMultiplier * decimalsA) / 100; - - uint256 amountAgToken = transmuter.quoteIn(swapAmount, address(eurA), address(agToken)); - uint256 amountOut = transmuter.quoteIn(amountAgToken, address(agToken), address(eurB)); - - assertEq(rebalancer.quoteIn(swapAmount, address(eurA), address(eurB)), amountOut); - - assertEq( - rebalancer.quoteIn(swapAmount, address(eurA), address(eurB)), - (multiplier * swapMultiplier * decimalsB) / 100 - ); - assertEq( - rebalancer.quoteIn(swapAmount, address(eurA), address(eurY)), - (multiplier * swapMultiplier * decimalsY) / 100 - ); - - swapAmount = (multiplier * swapMultiplier * decimalsY) / 100; - assertEq( - rebalancer.quoteIn(swapAmount, address(eurY), address(eurB)), - (multiplier * swapMultiplier * decimalsB) / 100 - ); - assertEq( - rebalancer.quoteIn(swapAmount, address(eurY), address(eurA)), - (multiplier * swapMultiplier * decimalsA) / 100 - ); - - swapAmount = (multiplier * swapMultiplier * decimalsB) / 100; - assertEq( - rebalancer.quoteIn(swapAmount, address(eurB), address(eurA)), - (multiplier * swapMultiplier * decimalsA) / 100 - ); - assertEq( - rebalancer.quoteIn(swapAmount, address(eurB), address(eurY)), - (multiplier * swapMultiplier * decimalsY) / 100 - ); - } - - function testFuzz_QuoteInWithSubsidy(uint256 multiplier, uint256 swapMultiplier, uint256 guaranteedRate) public { - multiplier = bound(multiplier, 10, 10 ** 6); - swapMultiplier = bound(swapMultiplier, 1, 100); - guaranteedRate = bound(guaranteedRate, 10 ** 15, 10 ** 21); - // let's first load the reserves of the protocol - _loadReserves(charlie, multiplier); - // Here there are no fees, and oracles are all constant -> so any mint and burn should be an onpar - // conversion for both tokens - - uint256 swapAmount = (multiplier * swapMultiplier * decimalsA) / 100; - - uint256 guaranteedAmountOut = (swapAmount * guaranteedRate * decimalsB) / (1e18 * decimalsA); - - // This is the amount we'd normally obtain after the swap - uint256 subsidyBudget = multiplier * swapMultiplier * 1e18; - - deal(address(agToken), address(rebalancer), subsidyBudget); - vm.startPrank(governor); - rebalancer.setOrder(address(eurA), address(eurB), subsidyBudget, guaranteedRate); - vm.stopPrank(); - - assertEq(rebalancer.getGuaranteedAmountOut(address(eurA), address(eurB), swapAmount), guaranteedAmountOut); - - uint256 amountAgToken = transmuter.quoteIn(swapAmount, address(eurA), address(agToken)); - uint256 amountAgTokenNeeded = transmuter.quoteOut(guaranteedAmountOut, address(agToken), address(eurB)); - uint256 subsidy; - if (amountAgTokenNeeded > amountAgToken) { - subsidy = amountAgTokenNeeded - amountAgToken; - if (subsidy > subsidyBudget) subsidy = subsidyBudget; - } - - amountAgToken += subsidy; - uint256 amountOut = transmuter.quoteIn(amountAgToken, address(agToken), address(eurB)); - assertEq(rebalancer.quoteIn(swapAmount, address(eurA), address(eurB)), amountOut); - } - - function test_QuoteInWithSubsidy() public { - // let's first load the reserves of the protocol - _loadReserves(charlie, 100); - // Here there are no fees, and oracles are all constant -> so any mint and burn should be an onpar - // conversion for both tokens - uint256 swapAmount = decimalsA; - - // 1 eurB = 10^(-2)*eurA; - uint256 guaranteedRate = 10 ** 16; - - uint256 guaranteedAmountOut = (swapAmount * guaranteedRate * decimalsB) / (1e18 * decimalsA); - - // This is the amount we'd normally obtain after the swap - uint256 subsidyBudget = 1e19; - - deal(address(agToken), address(rebalancer), 1000 * 10 ** 18); - vm.startPrank(governor); - rebalancer.setOrder(address(eurA), address(eurB), subsidyBudget, guaranteedRate); - vm.stopPrank(); - - assertEq(rebalancer.getGuaranteedAmountOut(address(eurA), address(eurB), swapAmount), guaranteedAmountOut); - - // Here we're better than the exchange rate -> so get normal value and does not consume the subsidy budget - assertEq(rebalancer.quoteIn(swapAmount, address(eurA), address(eurB)), decimalsB); - - guaranteedRate = 10 ** 19; - guaranteedAmountOut = (swapAmount * guaranteedRate * decimalsB) / (1e18 * decimalsA); - vm.startPrank(governor); - rebalancer.setOrder(address(eurA), address(eurB), subsidyBudget, guaranteedRate); - vm.stopPrank(); - - assertEq(rebalancer.quoteIn(swapAmount, address(eurA), address(eurB)), 10 * decimalsB); - - assertEq(rebalancer.quoteIn(swapAmount / 2, address(eurA), address(eurB)), (10 * decimalsB) / 2); - // Now if the swap amount is too big and empties the reserves - // You need to put in the whole budget to cover for this but cannot get the guaranteed rate - assertEq(rebalancer.quoteIn(swapAmount * 2, address(eurA), address(eurB)), 10 * decimalsB + 2 * decimalsB); - } - - /*////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - SWAP EXACT INPUT - //////////////////////////////////////////////////////////////////////////////////////////////////////////////////*/ - - function testFuzz_SwapExactInput_Revert(uint256 multiplier, uint256 swapMultiplier) public { - multiplier = bound(multiplier, 10, 10 ** 6); - swapMultiplier = bound(swapMultiplier, 1, 100); - // let's first load the reserves of the protocol - _loadReserves(charlie, multiplier); - uint256 swapAmount = (multiplier * swapMultiplier * decimalsA) / 100; - deal(address(eurA), address(charlie), swapAmount); - vm.startPrank(charlie); - eurA.approve(address(rebalancer), swapAmount); - vm.expectRevert(Errors.TooSmallAmountOut.selector); - rebalancer.swapExactInput( - swapAmount, - type(uint256).max, - address(eurA), - address(eurB), - address(bob), - block.timestamp * 2 - ); - uint256 curTimestamp = block.timestamp; - skip(curTimestamp + 1); - vm.expectRevert(Errors.TooLate.selector); - rebalancer.swapExactInput(swapAmount, 0, address(eurA), address(eurB), address(bob), block.timestamp - 1); - - vm.stopPrank(); - } - - function testFuzz_SwapExactInputWithoutSubsidy(uint256 multiplier, uint256 swapMultiplier) public { - multiplier = bound(multiplier, 10, 10 ** 6); - swapMultiplier = bound(swapMultiplier, 1, 100); - // let's first load the reserves of the protocol - _loadReserves(charlie, multiplier); - // Here there are no fees, and oracles are all constant -> so any mint and burn should be an onpar - // conversion for both tokens - - uint256 swapAmount = (multiplier * swapMultiplier * decimalsA) / 100; - uint256 amountOut = (multiplier * swapMultiplier * decimalsB) / 100; - assertEq(rebalancer.quoteIn(swapAmount, address(eurA), address(eurB)), amountOut); - deal(address(eurA), address(charlie), swapAmount * 2); - - vm.startPrank(charlie); - eurA.approve(address(rebalancer), swapAmount * 2); - uint256 amountOut2 = rebalancer.swapExactInput( - swapAmount, - 0, - address(eurA), - address(eurB), - address(bob), - block.timestamp * 2 - ); - vm.stopPrank(); - assertEq(amountOut2, amountOut); - assertEq(eurB.balanceOf(address(bob)), amountOut); - assertEq(eurA.allowance(address(rebalancer), address(transmuter)), type(uint256).max); - - amountOut = (multiplier * swapMultiplier * decimalsY) / 100; - vm.startPrank(charlie); - amountOut2 = rebalancer.swapExactInput( - swapAmount, - 0, - address(eurA), - address(eurY), - address(bob), - block.timestamp * 2 - ); - vm.stopPrank(); - assertEq(amountOut2, amountOut); - assertEq(eurY.balanceOf(address(bob)), amountOut); - assertEq(eurA.allowance(address(rebalancer), address(transmuter)), type(uint256).max); - assertEq(eurA.allowance(address(charlie), address(rebalancer)), 0); - } - - function testFuzz_SwapExactInputWithSubsidy( - uint256 multiplier, - uint256 swapMultiplier, - uint256 guaranteedRate - ) public { - multiplier = bound(multiplier, 10, 10 ** 6); - swapMultiplier = bound(swapMultiplier, 1, 100); - guaranteedRate = bound(guaranteedRate, 10 ** 15, 10 ** 21); - // let's first load the reserves of the protocol - _loadReserves(charlie, multiplier); - // Here there are no fees, and oracles are all constant -> so any mint and burn should be an onpar - // conversion for both tokens - - uint256 swapAmount = (multiplier * swapMultiplier * decimalsA) / 100; - - uint256 guaranteedAmountOut = (swapAmount * guaranteedRate * decimalsB) / (1e18 * decimalsA); - - deal(address(eurA), address(charlie), swapAmount * 2); - - // This is the amount we'd normally obtain after the swap - uint256 subsidyBudget = multiplier * swapMultiplier * 1e18; - - deal(address(agToken), address(rebalancer), subsidyBudget); - vm.startPrank(governor); - rebalancer.setOrder(address(eurA), address(eurB), subsidyBudget, guaranteedRate); - vm.stopPrank(); - - assertEq(rebalancer.getGuaranteedAmountOut(address(eurA), address(eurB), swapAmount), guaranteedAmountOut); - - uint256 amountAgToken = transmuter.quoteIn(swapAmount, address(eurA), address(agToken)); - uint256 amountAgTokenNeeded = transmuter.quoteOut(guaranteedAmountOut, address(agToken), address(eurB)); - uint256 subsidy; - if (amountAgTokenNeeded > amountAgToken) { - subsidy = amountAgTokenNeeded - amountAgToken; - if (subsidy > subsidyBudget) subsidy = subsidyBudget; - } - - amountAgToken += subsidy; - uint256 amountOut = transmuter.quoteIn(amountAgToken, address(agToken), address(eurB)); - assertEq(rebalancer.quoteIn(swapAmount, address(eurA), address(eurB)), amountOut); - - vm.startPrank(charlie); - eurA.approve(address(rebalancer), swapAmount * 2); - uint256 amountOut2 = rebalancer.swapExactInput( - swapAmount, - 0, - address(eurA), - address(eurB), - address(bob), - block.timestamp * 2 - ); - vm.stopPrank(); - - assertEq(amountOut2, amountOut); - assertEq(eurB.balanceOf(address(bob)), amountOut); - assertEq(eurA.allowance(address(rebalancer), address(transmuter)), type(uint256).max); - assertEq(eurB.balanceOf(address(rebalancer)), 0); - assertEq(IERC20(address(agToken)).balanceOf(address(rebalancer)), subsidyBudget - subsidy); - assertEq(rebalancer.budget(), subsidyBudget - subsidy); - (uint112 subsidyBudgetLeft, , , ) = rebalancer.orders(address(eurA), address(eurB)); - assertEq(subsidyBudgetLeft, subsidyBudget - subsidy); - } - - function test_SwapExactInputWithMultiplePartialSubsidies() public { - // let's first load the reserves of the protocol - _loadReserves(charlie, 100); - // Here there are no fees, and oracles are all constant -> so any mint and burn should be an onpar - // conversion for both tokens - uint256 swapAmount = decimalsA; - - // 1 eurB = 10^(-2)*eurA; - uint256 guaranteedRate = 10 ** 16; - - uint256 guaranteedAmountOut = (swapAmount * guaranteedRate * decimalsB) / (1e18 * decimalsA); - - // This is the amount we'd normally obtain after the swap - uint256 subsidyBudget = 1e19; - - deal(address(eurA), address(charlie), swapAmount * 3); - - deal(address(agToken), address(rebalancer), subsidyBudget); - vm.startPrank(governor); - rebalancer.setOrder(address(eurA), address(eurB), subsidyBudget, guaranteedRate); - vm.stopPrank(); - - assertEq(rebalancer.getGuaranteedAmountOut(address(eurA), address(eurB), swapAmount), guaranteedAmountOut); - - uint256 amountOut = decimalsB; - - // Here we're better than the exchange rate -> so get normal value and does not consume the subsidy budget - assertEq(rebalancer.quoteIn(swapAmount, address(eurA), address(eurB)), decimalsB); - vm.startPrank(charlie); - eurA.approve(address(rebalancer), swapAmount * 4); - uint256 amountOut2 = rebalancer.swapExactInput( - swapAmount, - 0, - address(eurA), - address(eurB), - address(bob), - block.timestamp * 2 - ); - vm.stopPrank(); - - assertEq(amountOut2, amountOut); - assertEq(eurA.allowance(address(rebalancer), address(transmuter)), type(uint256).max); - assertEq(eurA.balanceOf(address(charlie)), swapAmount * 2); - assertEq(eurB.balanceOf(address(bob)), amountOut); - assertEq(eurB.balanceOf(address(rebalancer)), 0); - assertEq(IERC20(address(agToken)).balanceOf(address(rebalancer)), subsidyBudget); - assertEq(rebalancer.budget(), subsidyBudget); - (uint112 subsidyBudgetLeft, , , ) = rebalancer.orders(address(eurA), address(eurB)); - assertEq(subsidyBudgetLeft, subsidyBudget); - - guaranteedRate = 10 ** 19; - guaranteedAmountOut = (swapAmount * guaranteedRate * decimalsB) / (1e18 * decimalsA); - vm.startPrank(governor); - rebalancer.setOrder(address(eurA), address(eurB), subsidyBudget, guaranteedRate); - vm.stopPrank(); - amountOut = (10 * decimalsB) / 2; - - assertEq(rebalancer.quoteIn(swapAmount / 2, address(eurA), address(eurB)), amountOut); - assertEq(rebalancer.quoteIn(swapAmount, address(eurA), address(eurB)), 10 * decimalsB); - assertEq(rebalancer.quoteIn(swapAmount * 2, address(eurA), address(eurB)), 10 * decimalsB + 2 * decimalsB); - - vm.startPrank(charlie); - amountOut2 = rebalancer.swapExactInput( - swapAmount / 2, - 0, - address(eurA), - address(eurB), - address(bob), - block.timestamp * 2 - ); - vm.stopPrank(); - // This should yield decimalsB * 10 / 2, and sponsorship for this should be decimalsB * 9 / 2 because swap - // gives decimals / 2 - - assertEq(amountOut2, amountOut); - assertEq(eurA.allowance(address(rebalancer), address(transmuter)), type(uint256).max); - assertEq(eurA.balanceOf(address(charlie)), (swapAmount * 3) / 2); - assertEq(eurB.balanceOf(address(bob)), decimalsB + (10 * decimalsB) / 2); - assertEq(eurB.balanceOf(address(rebalancer)), 0); - assertEq(IERC20(address(agToken)).balanceOf(address(rebalancer)), (subsidyBudget * 55) / 100); - assertEq(rebalancer.budget(), (subsidyBudget * 55) / 100); - (subsidyBudgetLeft, , , ) = rebalancer.orders(address(eurA), address(eurB)); - assertEq(subsidyBudgetLeft, (subsidyBudget * 55) / 100); - - vm.startPrank(charlie); - amountOut2 = rebalancer.swapExactInput( - (swapAmount * 3) / 2, - 0, - address(eurA), - address(eurB), - address(bob), - block.timestamp * 2 - ); - vm.stopPrank(); - amountOut = (3 * decimalsB) / 2 + (decimalsB * 55) / 10; - - assertEq(amountOut2, amountOut); - assertEq(eurA.allowance(address(rebalancer), address(transmuter)), type(uint256).max); - assertEq(eurA.balanceOf(address(charlie)), 0); - assertEq(eurB.balanceOf(address(bob)), decimalsB + (10 * decimalsB) / 2 + amountOut); - assertEq(eurB.balanceOf(address(rebalancer)), 0); - assertEq(IERC20(address(agToken)).balanceOf(address(rebalancer)), 0); - assertEq(rebalancer.budget(), 0); - (subsidyBudgetLeft, , , ) = rebalancer.orders(address(eurA), address(eurB)); - assertEq(subsidyBudgetLeft, 0); - } - - /*////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - UTILS - //////////////////////////////////////////////////////////////////////////////////////////////////////////////////*/ - - function _loadReserves( - address owner, - uint256 multiplier - ) internal returns (uint256 mintedStables, uint256[] memory collateralMintedStables) { - collateralMintedStables = new uint256[](_collaterals.length); - - vm.startPrank(owner); - for (uint256 i; i < _collaterals.length; i++) { - uint256 amount = multiplier * 100000 * 10 ** IERC20Metadata(_collaterals[i]).decimals(); - deal(_collaterals[i], owner, amount); - IERC20(_collaterals[i]).approve(address(transmuter), amount); - - collateralMintedStables[i] = transmuter.swapExactInput( - amount, - 0, - _collaterals[i], - address(agToken), - owner, - block.timestamp * 2 - ); - mintedStables += collateralMintedStables[i]; - } - vm.stopPrank(); - } -} diff --git a/test/fuzz/RebalancerFlashloanVaultTest.t.sol b/test/fuzz/RebalancerFlashloanVaultTest.t.sol deleted file mode 100644 index fe80f033..00000000 --- a/test/fuzz/RebalancerFlashloanVaultTest.t.sol +++ /dev/null @@ -1,89 +0,0 @@ -// 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/RebalancerFlashloanVault.sol"; - -contract RebalancerFlashloanVaultTest is Fixture, FunctionUtils { - using SafeERC20 for IERC20; - - RebalancerFlashloanVault 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 RebalancerFlashloanVault(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 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, new bytes(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/HarvesterSwapUSDATest.t.sol b/test/scripts/HarvesterSwapUSDATest.t.sol deleted file mode 100644 index 5736a7a4..00000000 --- a/test/scripts/HarvesterSwapUSDATest.t.sol +++ /dev/null @@ -1,136 +0,0 @@ -// 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 deleted file mode 100644 index 13942147..00000000 --- a/test/scripts/HarvesterUSDATest.t.sol +++ /dev/null @@ -1,237 +0,0 @@ -// 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 { HarvesterVault } from "contracts/helpers/HarvesterVault.sol"; - -import { RebalancerFlashloanVault, IERC4626, IERC3156FlashLender } from "contracts/helpers/RebalancerFlashloanVault.sol"; - -interface IFlashAngle { - function addStablecoinSupport(address _treasury) external; - - function setFlashLoanParameters(address stablecoin, uint64 _flashLoanFee, uint256 _maxBorrowable) external; -} - -contract HarvesterUSDATest is Test { - using stdJson for string; - - ITransmuter transmuter; - IERC20 USDA; - IAgToken treasuryUSDA; - IFlashAngle FLASHLOAN; - address governor; - RebalancerFlashloanVault public rebalancer; - uint256 ethereumFork; - HarvesterVault harvester; - uint64 public targetExposure; - uint64 public maxExposureYieldAsset; - uint64 public minExposureYieldAsset; - - function setUp() public { - ethereumFork = vm.createSelectFork(vm.envString("ETH_NODE_URI_MAINNET"), 19939091); - - transmuter = ITransmuter(0x222222fD79264BBE280b4986F6FEfBC3524d0137); - USDA = IERC20(0x0000206329b97DB379d5E1Bf586BbDB969C63274); - FLASHLOAN = IFlashAngle(0x4A2FF9bC686A0A23DA13B6194C69939189506F7F); - treasuryUSDA = IAgToken(0xf8588520E760BB0b3bDD62Ecb25186A28b0830ee); - governor = 0xdC4e6DFe07EFCa50a197DF15D9200883eF4Eb1c8; - // 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 HarvesterVault( - address(rebalancer), - address(STEAK_USDC), - targetExposure, - 1, - maxExposureYieldAsset, - minExposureYieldAsset, - 1e8 - ); - - 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); - - vm.stopPrank(); - } - - function testUnit_Harvest_IncreaseUSDCExposure() external { - // At current block: USDC exposure = 7.63%, steakUSDC = 75.26%, bIB01 = 17.11% - (uint256 fromUSDC, uint256 total) = transmuter.getIssuedByCollateral(address(USDC)); - (uint256 fromSTEAK, ) = transmuter.getIssuedByCollateral(address(STEAK_USDC)); - harvester.harvest(USDC, 1e9, new bytes(0)); - (uint256 fromUSDC2, uint256 total2) = transmuter.getIssuedByCollateral(address(USDC)); - (uint256 fromSTEAK2, ) = transmuter.getIssuedByCollateral(address(STEAK_USDC)); - assertGt(fromUSDC2, fromUSDC); - assertGt(fromSTEAK, fromSTEAK2); - assertGt(total, total2); - assertApproxEqRel((fromUSDC2 * 1e9) / total2, targetExposure, 100 * BPS); - assertApproxEqRel(fromUSDC2 * 1e9, targetExposure * total, 100 * BPS); - - 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); - assertGt(fromSTEAK2, fromSTEAK3); - assertGt(total2, total3); - assertGt((fromUSDC3 * 1e9) / total3, (fromUSDC2 * 1e9) / total2); - assertApproxEqRel((fromUSDC3 * 1e9) / total3, (fromUSDC2 * 1e9) / total2, 10 * BPS); - assertGt(targetExposure, (fromUSDC3 * 1e9) / total3); - } - - function testUnit_Harvest_IncreaseUSDCExposureButMinValueYield() external { - // At current block: USDC exposure = 7.63%, steakUSDC = 75.26%, bIB01 = 17.11% -> putting below - // min exposure - vm.startPrank(governor); - harvester.setCollateralData(STEAK_USDC, targetExposure, (80 * 1e9) / 100, (90 * 1e9) / 100, 1); - vm.stopPrank(); - (uint256 fromUSDC, uint256 total) = transmuter.getIssuedByCollateral(address(USDC)); - (uint256 fromSTEAK, ) = transmuter.getIssuedByCollateral(address(STEAK_USDC)); - harvester.harvest(USDC, 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, 1e9, new bytes(0)); - (uint256 fromUSDC3, uint256 total3) = transmuter.getIssuedByCollateral(address(USDC)); - (uint256 fromSTEAK3, ) = transmuter.getIssuedByCollateral(address(STEAK_USDC)); - assertEq(fromUSDC3, fromUSDC2); - assertEq(fromSTEAK2, fromSTEAK3); - assertEq(total2, total3); - } - - function testUnit_Harvest_IncreaseUSDCExposureButMinValueThresholdReached() external { - // At current block: USDC exposure = 7.63%, steakUSDC = 75.26%, bIB01 = 17.11% -> putting in between - // min exposure and target exposure - vm.startPrank(governor); - harvester.setCollateralData(STEAK_USDC, targetExposure, (73 * 1e9) / 100, (90 * 1e9) / 100, 1); - vm.stopPrank(); - - (uint256 fromUSDC, uint256 total) = transmuter.getIssuedByCollateral(address(USDC)); - (uint256 fromSTEAK, ) = transmuter.getIssuedByCollateral(address(STEAK_USDC)); - harvester.harvest(USDC, 1e9, new bytes(0)); - (uint256 fromUSDC2, uint256 total2) = transmuter.getIssuedByCollateral(address(USDC)); - (uint256 fromSTEAK2, ) = transmuter.getIssuedByCollateral(address(STEAK_USDC)); - assertGt(fromUSDC2, fromUSDC); - assertGt(fromSTEAK, fromSTEAK2); - assertGt(total, total2); - assertApproxEqRel((fromSTEAK2 * 1e9) / total2, (73 * 1e9) / 100, 100 * BPS); - - harvester.harvest(USDC, 1e9, new bytes(0)); - (uint256 fromUSDC3, uint256 total3) = transmuter.getIssuedByCollateral(address(USDC)); - (uint256 fromSTEAK3, ) = transmuter.getIssuedByCollateral(address(STEAK_USDC)); - assertGt(fromUSDC3, fromUSDC2); - assertGt(fromSTEAK2, fromSTEAK3); - assertGt(total2, total3); - assertApproxEqRel((fromSTEAK3 * 1e9) / total3, (fromSTEAK2 * 1e9) / total2, 10 * BPS); - } - - function testUnit_Harvest_DecreaseUSDCExposureClassical() external { - // At current block: USDC exposure = 7.63%, steakUSDC = 75.26%, bIB01 = 17.11% -> putting below target - vm.startPrank(governor); - harvester.setCollateralData(STEAK_USDC, (5 * 1e9) / 100, (73 * 1e9) / 100, (90 * 1e9) / 100, 1); - vm.stopPrank(); - - (uint256 fromUSDC, uint256 total) = transmuter.getIssuedByCollateral(address(USDC)); - (uint256 fromSTEAK, ) = transmuter.getIssuedByCollateral(address(STEAK_USDC)); - - harvester.harvest(USDC, 1e9, new bytes(0)); - - (uint256 fromUSDC2, uint256 total2) = transmuter.getIssuedByCollateral(address(USDC)); - (uint256 fromSTEAK2, ) = transmuter.getIssuedByCollateral(address(STEAK_USDC)); - assertGt(fromUSDC, fromUSDC2); - assertGt(fromSTEAK2, fromSTEAK); - assertGt(total, total2); - assertApproxEqRel((fromUSDC2 * 1e9) / total2, (5 * 1e9) / 100, 100 * BPS); - - harvester.harvest(USDC, 1e9, new bytes(0)); - (uint256 fromUSDC3, uint256 total3) = transmuter.getIssuedByCollateral(address(USDC)); - (uint256 fromSTEAK3, ) = transmuter.getIssuedByCollateral(address(STEAK_USDC)); - assertGt(fromUSDC2, fromUSDC3); - assertGt(fromSTEAK3, fromSTEAK2); - assertGt(total2, total3); - assertGe((fromUSDC2 * 1e9) / total2, (fromUSDC3 * 1e9) / total3); - assertGe((fromUSDC3 * 1e9) / total3, (5 * 1e9) / 100); - } - - function testUnit_Harvest_DecreaseUSDCExposureAlreadyMaxThreshold() external { - // At current block: USDC exposure = 7.63%, steakUSDC = 75.26%, bIB01 = 17.11% -> putting below target - vm.startPrank(governor); - harvester.setCollateralData(STEAK_USDC, (5 * 1e9) / 100, (73 * 1e9) / 100, (74 * 1e9) / 100, 1); - vm.stopPrank(); - - (uint256 fromUSDC, uint256 total) = transmuter.getIssuedByCollateral(address(USDC)); - (uint256 fromSTEAK, ) = transmuter.getIssuedByCollateral(address(STEAK_USDC)); - - harvester.harvest(USDC, 1e9, new bytes(0)); - - (uint256 fromUSDC2, uint256 total2) = transmuter.getIssuedByCollateral(address(USDC)); - (uint256 fromSTEAK2, ) = transmuter.getIssuedByCollateral(address(STEAK_USDC)); - assertEq(fromUSDC, fromUSDC2); - assertEq(fromSTEAK2, fromSTEAK); - assertEq(total, total2); - - harvester.harvest(USDC, 1e9, new bytes(0)); - (uint256 fromUSDC3, uint256 total3) = transmuter.getIssuedByCollateral(address(USDC)); - (uint256 fromSTEAK3, ) = transmuter.getIssuedByCollateral(address(STEAK_USDC)); - assertEq(fromUSDC2, fromUSDC3); - assertEq(fromSTEAK3, fromSTEAK2); - assertEq(total2, total3); - } - - function testUnit_Harvest_DecreaseUSDCExposureTillMaxThreshold() external { - // At current block: USDC exposure = 7.63%, steakUSDC = 75.26%, bIB01 = 17.11% -> putting below target - vm.startPrank(governor); - harvester.setCollateralData(STEAK_USDC, (5 * 1e9) / 100, (73 * 1e9) / 100, (755 * 1e9) / 1000, 1); - vm.stopPrank(); - - (uint256 fromUSDC, uint256 total) = transmuter.getIssuedByCollateral(address(USDC)); - (uint256 fromSTEAK, ) = transmuter.getIssuedByCollateral(address(STEAK_USDC)); - - harvester.harvest(USDC, 1e9, new bytes(0)); - - (uint256 fromUSDC2, uint256 total2) = transmuter.getIssuedByCollateral(address(USDC)); - (uint256 fromSTEAK2, ) = transmuter.getIssuedByCollateral(address(STEAK_USDC)); - assertGt(fromUSDC, fromUSDC2); - assertGt(fromSTEAK2, fromSTEAK); - assertGt(total, total2); - assertLe((fromSTEAK2 * 1e9) / total2, (755 * 1e9) / 1000); - assertApproxEqRel((fromSTEAK2 * 1e9) / total2, (755 * 1e9) / 1000, 100 * BPS); - - harvester.harvest(USDC, 1e9, new bytes(0)); - - (uint256 fromUSDC3, uint256 total3) = transmuter.getIssuedByCollateral(address(USDC)); - (uint256 fromSTEAK3, ) = transmuter.getIssuedByCollateral(address(STEAK_USDC)); - assertGt(fromUSDC2, fromUSDC3); - assertGt(fromSTEAK3, fromSTEAK2); - assertGt(total2, total3); - assertApproxEqRel((fromSTEAK3 * 1e9) / total3, (fromSTEAK2 * 1e9) / total2, 10 * BPS); - } -} diff --git a/test/scripts/RebalancerSwapUSDATest.t.sol b/test/scripts/RebalancerSwapUSDATest.t.sol deleted file mode 100644 index dd5bede4..00000000 --- a/test/scripts/RebalancerSwapUSDATest.t.sol +++ /dev/null @@ -1,452 +0,0 @@ -// 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 deleted file mode 100644 index 65de47f9..00000000 --- a/test/scripts/RebalancerUSDATest.t.sol +++ /dev/null @@ -1,322 +0,0 @@ -// 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 { RebalancerFlashloanVault, IERC4626, IERC3156FlashLender } from "contracts/helpers/RebalancerFlashloanVault.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; - RebalancerFlashloanVault 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 RebalancerFlashloanVault( - // 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, new bytes(0)); - - vm.expectRevert(); - rebalancer.adjustYieldExposure(BASE_18, 0, USDC, STEAK_USDC, 0, new bytes(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, new bytes(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, new bytes(0)); - 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, new bytes(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, new bytes(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, new bytes(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, new bytes(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, new bytes(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, new bytes(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, new bytes(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, new bytes(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(); - } -} From 7b0d45596ba0196a01b51b0113bdb80e61e0e410 Mon Sep 17 00:00:00 2001 From: 0xtekgrinder <0xtekgrinder@protonmail.com> Date: Tue, 8 Oct 2024 10:35:08 +0200 Subject: [PATCH 27/69] feat: computeRebalanceAmount public function --- contracts/helpers/BaseHarvester.sol | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/contracts/helpers/BaseHarvester.sol b/contracts/helpers/BaseHarvester.sol index 5703e63a..632ffd71 100644 --- a/contracts/helpers/BaseHarvester.sol +++ b/contracts/helpers/BaseHarvester.sol @@ -109,6 +109,20 @@ abstract contract BaseHarvester is IHarvester, AccessControl { _setMaxSlippage(newMaxSlippage); } + /*////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + VIEW FUNCTIONS + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////*/ + + /** + * @notice Compute the amount needed to rebalance the Transmuter + * @param yieldBearingAsset address of the yield bearing asset + * @return increase whether the exposure should be increased + * @return amount amount to be rebalanced + */ + function computeRebalanceAmount(address yieldBearingAsset) external view returns (uint8 increase, uint256 amount) { + return _computeRebalanceAmount(yieldBearingAsset, yieldBearingData[yieldBearingAsset]); + } + /*////////////////////////////////////////////////////////////////////////////////////////////////////////////////// INTERNAL FUNCTIONS //////////////////////////////////////////////////////////////////////////////////////////////////////////////////*/ From 36d22ea11a22ebf9e710ec99a31b90c034bead2f Mon Sep 17 00:00:00 2001 From: 0xtekgrinder <0xtekgrinder@protonmail.com> Date: Tue, 8 Oct 2024 12:07:13 +0200 Subject: [PATCH 28/69] fix: correct rebalancing computation + min/max for stablecoin --- contracts/helpers/BaseHarvester.sol | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/contracts/helpers/BaseHarvester.sol b/contracts/helpers/BaseHarvester.sol index 632ffd71..0941b4e4 100644 --- a/contracts/helpers/BaseHarvester.sol +++ b/contracts/helpers/BaseHarvester.sol @@ -16,9 +16,9 @@ struct YieldBearingParams { address stablecoin; // Target exposure to the collateral yield bearing asset used uint64 targetExposure; - // Maximum exposure within the Transmuter to the yield bearing asset + // Maximum exposure within the Transmuter to the stablecoin asset uint64 maxExposureYieldAsset; - // Minimum exposure within the Transmuter to the yield bearing asset + // Minimum exposure within the Transmuter to the stablecoin 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 @@ -69,8 +69,8 @@ abstract contract BaseHarvester is IHarvester, AccessControl { * @param yieldBearingAsset address of the yieldBearingAsset * @param stablecoin address of the stablecoin * @param targetExposure target exposure to the yieldBearingAsset asset used - * @param minExposureYieldAsset minimum exposure within the Transmuter to the asset - * @param maxExposureYieldAsset maximum exposure within the Transmuter to the asset + * @param minExposureYieldAsset minimum exposure within the Transmuter to the stablecoin + * @param maxExposureYieldAsset maximum exposure within the Transmuter to the stablecoin * @param overrideExposures whether limit exposures should be overriden or read onchain through the Transmuter */ function setYieldBearingAssetData( @@ -92,13 +92,13 @@ abstract contract BaseHarvester is IHarvester, AccessControl { } /** - * @notice Set the limit exposures to the yield bearing asset + * @notice Set the limit exposures to the stablecoin linked to the yield bearing asset * @param yieldBearingAsset address of the yield bearing asset */ function updateLimitExposuresYieldAsset(address yieldBearingAsset) public virtual onlyGuardian { YieldBearingParams storage yieldBearingInfo = yieldBearingData[yieldBearingAsset]; if (yieldBearingInfo.overrideExposures == 2) - _updateLimitExposuresYieldAsset(yieldBearingAsset, yieldBearingInfo); + _updateLimitExposuresYieldAsset(yieldBearingInfo.stablecoin, yieldBearingInfo); } /** @@ -176,22 +176,22 @@ abstract contract BaseHarvester is IHarvester, AccessControl { yieldBearingInfo.minExposureYieldAsset = minExposureYieldAsset; } else { yieldBearingInfo.overrideExposures = 2; - _updateLimitExposuresYieldAsset(yieldBearingAsset, yieldBearingInfo); + _updateLimitExposuresYieldAsset(yieldBearingInfo.stablecoin, yieldBearingInfo); } } function _updateLimitExposuresYieldAsset( - address yieldBearingAsset, + address stablecoin, YieldBearingParams storage yieldBearingInfo ) internal virtual { uint64[] memory xFeeMint; - (xFeeMint, ) = transmuter.getCollateralMintFees(yieldBearingAsset); + (xFeeMint, ) = transmuter.getCollateralMintFees(stablecoin); uint256 length = xFeeMint.length; if (length <= 1) yieldBearingInfo.maxExposureYieldAsset = 1e9; else yieldBearingInfo.maxExposureYieldAsset = xFeeMint[length - 2]; uint64[] memory xFeeBurn; - (xFeeBurn, ) = transmuter.getCollateralBurnFees(yieldBearingAsset); + (xFeeBurn, ) = transmuter.getCollateralBurnFees(stablecoin); length = xFeeBurn.length; if (length <= 1) yieldBearingInfo.minExposureYieldAsset = 0; else yieldBearingInfo.minExposureYieldAsset = xFeeBurn[length - 2]; From e01866c5145ea4b53960bdf761d537f13cfd1d2b Mon Sep 17 00:00:00 2001 From: 0xtekgrinder <0xtekgrinder@protonmail.com> Date: Tue, 8 Oct 2024 17:13:02 +0200 Subject: [PATCH 29/69] tests: add more complete tests for the harvesting of the different assets and multple scenarios --- test/fuzz/MultiBlockHarvester.t.sol | 258 ++++++++++++++++++++++------ 1 file changed, 207 insertions(+), 51 deletions(-) diff --git a/test/fuzz/MultiBlockHarvester.t.sol b/test/fuzz/MultiBlockHarvester.t.sol index 4ebf18b1..e0a0d600 100644 --- a/test/fuzz/MultiBlockHarvester.t.sol +++ b/test/fuzz/MultiBlockHarvester.t.sol @@ -108,7 +108,7 @@ contract MultiBlockHarvestertTest is Fixture, FunctionUtils { oracleXEVT = AggregatorV3Interface(address(new MockChainlinkOracle())); circuitChainlink[0] = AggregatorV3Interface(oracleXEVT); readData = abi.encode(circuitChainlink, stalePeriods, circuitChainIsMultiplied, chainlinkDecimals, quoteType); - MockChainlinkOracle(address(oracleXEVT)).setLatestAnswer(int256(IERC4626(XEVT).convertToAssets(BASE_8))); + MockChainlinkOracle(address(oracleXEVT)).setLatestAnswer(int256(BASE_8)); transmuter.setOracle( XEVT, abi.encode( @@ -148,7 +148,7 @@ contract MultiBlockHarvestertTest is Fixture, FunctionUtils { transmuter.togglePause(USDC, ActionType.Burn); transmuter.setStablecoinCap(USDC, type(uint256).max); - // mock isAllowed(address) returns (bool) + // mock isAllowed(address) returns (bool) to transfer XEVT vm.mockCall( 0x9019Fd383E490B4B045130707C9A1227F36F4636, abi.encodeWithSelector(Wow.isAllowed.selector), @@ -167,7 +167,7 @@ contract MultiBlockHarvestertTest is Fixture, FunctionUtils { vm.stopPrank(); targetExposure = uint64((15 * 1e9) / 100); - maxExposureYieldAsset = uint64((80 * 1e9) / 100); + maxExposureYieldAsset = uint64((90 * 1e9) / 100); minExposureYieldAsset = uint64((5 * 1e9) / 100); harvester = new MultiBlockHarvester(100_000e18, 1e8, accessControlManager, agToken, transmuter); @@ -211,6 +211,9 @@ contract MultiBlockHarvestertTest is Fixture, FunctionUtils { vm.expectRevert(Errors.NotGovernorOrGuardian.selector); harvester.updateLimitExposuresYieldAsset(address(XEVT)); + + vm.expectRevert(Errors.NotGovernorOrGuardian.selector); + harvester.toggleTrusted(alice); } function test_SettersHarvester() public { @@ -257,19 +260,6 @@ contract MultiBlockHarvestertTest is Fixture, FunctionUtils { } function test_UpdateLimitExposuresYieldAsset() public { - bytes memory data; - address _savingImplementation = address(new Savings()); - Savings newVault = Savings(_deployUpgradeable(address(proxyAdmin), _savingImplementation, data)); - string memory _name = "savingAgEUR"; - string memory _symbol = "SAGEUR"; - - vm.startPrank(governor); - MockTokenPermit(address(eurA)).mint(governor, 1e12); - eurA.approve(address(newVault), 1e12); - newVault.initialize(accessControlManager, IERC20MetadataUpgradeable(address(eurA)), _name, _symbol, BASE_6); - transmuter.addCollateral(address(newVault)); - vm.stopPrank(); - uint64[] memory xFeeMint = new uint64[](3); int64[] memory yFeeMint = new int64[](3); @@ -293,46 +283,33 @@ contract MultiBlockHarvestertTest is Fixture, FunctionUtils { yFeeBurn[2] = int64(uint64(BASE_9 / 10)); vm.startPrank(governor); - transmuter.setFees(address(newVault), xFeeBurn, yFeeBurn, false); - transmuter.setFees(address(newVault), xFeeMint, yFeeMint, true); + transmuter.setFees(address(EURC), xFeeBurn, yFeeBurn, false); + transmuter.setFees(address(EURC), xFeeMint, yFeeMint, true); harvester.setYieldBearingAssetData( - address(newVault), - address(eurA), + address(XEVT), + address(EURC), targetExposure, minExposureYieldAsset, maxExposureYieldAsset, 0 ); - harvester.updateLimitExposuresYieldAsset(address(newVault)); + harvester.updateLimitExposuresYieldAsset(address(XEVT)); - (, , uint64 maxi, uint64 mini, ) = harvester.yieldBearingData(address(newVault)); + (, , uint64 maxi, uint64 mini, ) = harvester.yieldBearingData(address(XEVT)); assertEq(maxi, (15 * BASE_9) / 100); assertEq(mini, BASE_9 / 10); vm.stopPrank(); } - function test_FinalizeRebalance_IncreaseExposureXEVT(uint256 amount) external { - _loadReserve(XEVT, 1e26); - amount = bound(amount, 1e18, 1e24); - deal(XEVT, address(harvester), amount); - - vm.prank(alice); - harvester.finalizeRebalance(XEVT, amount); - - assertEq(agToken.balanceOf(address(harvester)), 0); - assertEq(IERC20(XEVT).balanceOf(address(harvester)), 0); - } - - function test_FinalizeRebalance_DecreaseExposureEURC(uint256 amount) external { - _loadReserve(EURC, 1e26); - amount = bound(amount, 1e18, 1e24); - deal(EURC, address(harvester), amount); + function test_ToggleTrusted() public { + vm.startPrank(governor); + harvester.toggleTrusted(bob); + assertEq(harvester.isTrusted(bob), true); - vm.prank(alice); - harvester.finalizeRebalance(EURC, amount); + harvester.toggleTrusted(bob); + assertEq(harvester.isTrusted(bob), false); - assertEq(agToken.balanceOf(address(harvester)), 0); - assertEq(IERC20(EURC).balanceOf(address(harvester)), 0); + vm.stopPrank(); } function test_harvest_TooBigMintedAmount() external { @@ -346,42 +323,211 @@ contract MultiBlockHarvestertTest is Fixture, FunctionUtils { } function test_harvest_IncreaseExposureXEVT(uint256 amount) external { - _loadReserve(EURC, 1e11); - _loadReserve(XEVT, 1e6); + amount = bound(amount, 1e3, 1e11); + _loadReserve(EURC, amount); _setYieldBearingData(XEVT, EURC); + (uint256 issuedFromYieldBearingAssetBefore, ) = transmuter.getIssuedByCollateral(XEVT); + (uint256 issuedFromStablecoinBefore, uint256 totalIssuedBefore) = transmuter.getIssuedByCollateral(EURC); + (uint8 expectedIncrease, uint256 expectedAmount) = harvester.computeRebalanceAmount(XEVT); + + assertEq(expectedIncrease, 1); + assertEq(expectedAmount, (amount * 1e12 * targetExposure) / 1e9); + assertEq(issuedFromStablecoinBefore, amount * 1e12); + assertEq(issuedFromYieldBearingAssetBefore, 0); + assertEq(totalIssuedBefore, issuedFromStablecoinBefore); + vm.prank(alice); harvester.harvest(XEVT, 1e9, new bytes(0)); + + assertEq(IERC20(XEVT).balanceOf(address(harvester)), 0); + assertEq(IERC20(EURC).balanceOf(address(harvester)), 0); + assertEq(agToken.balanceOf(address(harvester)), 0); + + (uint256 issuedFromYieldBearingAsset, uint256 totalIssued) = transmuter.getIssuedByCollateral(XEVT); + (uint256 issuedFromStablecoin, ) = transmuter.getIssuedByCollateral(EURC); + assertEq(totalIssued, issuedFromStablecoin + issuedFromYieldBearingAsset); + assertApproxEqRel(issuedFromStablecoin, (totalIssued * (1e9 - targetExposure)) / 1e9, 1e18); + assertApproxEqRel(issuedFromYieldBearingAsset, (totalIssued * targetExposure) / 1e9, 1e18); + + (uint8 increase, uint256 amount) = harvester.computeRebalanceAmount(XEVT); + assertEq(increase, 1); // There is still a small amount to mint because of the transmuter fees and slippage } function test_harvest_DecreaseExposureXEVT(uint256 amount) external { - _loadReserve(EURC, 1e11); - _loadReserve(XEVT, 1e8); + amount = bound(amount, 1e3, 1e11); + _loadReserve(XEVT, amount); _setYieldBearingData(XEVT, EURC); + (uint256 issuedFromYieldBearingAssetBefore, ) = transmuter.getIssuedByCollateral(XEVT); + (uint256 issuedFromStablecoinBefore, uint256 totalIssuedBefore) = transmuter.getIssuedByCollateral(EURC); + (uint8 expectedIncrease, uint256 expectedAmount) = harvester.computeRebalanceAmount(XEVT); + + assertEq(expectedIncrease, 0); + assertEq(issuedFromStablecoinBefore, 0); + assertEq(issuedFromYieldBearingAssetBefore, amount * 1e12); + assertEq(totalIssuedBefore, issuedFromYieldBearingAssetBefore); + assertEq(expectedAmount, issuedFromYieldBearingAssetBefore - ((targetExposure * totalIssuedBefore) / 1e9)); + vm.prank(alice); harvester.harvest(XEVT, 1e9, new bytes(0)); + + assertEq(IERC20(EURC).balanceOf(address(harvester)), 0); + assertApproxEqRel(IERC20(XEVT).balanceOf(address(harvester)), expectedAmount / 1e12, 1e18); // XEVT is stored in the harvester while the redemption is in progress + assertEq(agToken.balanceOf(address(harvester)), 0); + + // fake semd EURC to harvester + deal(EURC, address(harvester), amount); + + vm.prank(alice); + harvester.finalizeRebalance(EURC, amount); + + assertEq(agToken.balanceOf(address(harvester)), 0); + assertEq(IERC20(EURC).balanceOf(address(harvester)), 0); + + (uint256 issuedFromYieldBearingAsset, uint256 totalIssued) = transmuter.getIssuedByCollateral(XEVT); + (uint256 issuedFromStablecoin, ) = transmuter.getIssuedByCollateral(EURC); + assertEq(totalIssued, issuedFromStablecoin + issuedFromYieldBearingAsset); + assertApproxEqRel(issuedFromStablecoin, (totalIssued * (1e9 - targetExposure)) / 1e9, 1e18); + assertApproxEqRel(issuedFromYieldBearingAsset, (totalIssued * targetExposure) / 1e9, 1e18); + + (uint8 increase, uint256 amount) = harvester.computeRebalanceAmount(XEVT); + assertEq(increase, 1); // There is still a small amount to mint because of the transmuter fees and slippage } function test_harvest_IncreaseExposureUSDM(uint256 amount) external { - _loadReserve(USDC, 1e11); - _loadReserve(USDM, 1e6); + amount = bound(amount, 1e3, 1e11); + _loadReserve(USDC, amount); _setYieldBearingData(USDM, USDC); + (uint256 issuedFromYieldBearingAssetBefore, ) = transmuter.getIssuedByCollateral(USDM); + (uint256 issuedFromStablecoinBefore, uint256 totalIssuedBefore) = transmuter.getIssuedByCollateral(USDC); + (uint8 expectedIncrease, uint256 expectedAmount) = harvester.computeRebalanceAmount(USDM); + + assertEq(expectedIncrease, 1); + assertEq(issuedFromStablecoinBefore, amount * 1e12); + assertEq(issuedFromYieldBearingAssetBefore, 0); + assertEq(totalIssuedBefore, issuedFromStablecoinBefore); + assertEq(expectedAmount, (amount * 1e12 * targetExposure) / 1e9); + vm.prank(alice); harvester.harvest(USDM, 1e9, new bytes(0)); + + assertEq(IERC20(USDC).balanceOf(address(harvester)), 0); + assertEq(IERC20(USDM).balanceOf(address(harvester)), 0); + assertApproxEqRel(IERC20(USDM).balanceOf(address(receiver)), expectedAmount, 1e18); + assertEq(agToken.balanceOf(address(harvester)), 0); + + // fake semd USDC to harvester + deal(USDC, address(harvester), amount); + + vm.prank(alice); + harvester.finalizeRebalance(USDC, amount); + + assertEq(agToken.balanceOf(address(harvester)), 0); + assertEq(IERC20(USDC).balanceOf(address(harvester)), 0); + + (uint256 issuedFromYieldBearingAsset, uint256 totalIssued) = transmuter.getIssuedByCollateral(USDM); + (uint256 issuedFromStablecoin, ) = transmuter.getIssuedByCollateral(USDC); + assertEq(totalIssued, issuedFromStablecoin + issuedFromYieldBearingAsset); + assertApproxEqRel(issuedFromStablecoin, (totalIssued * (1e9 - targetExposure)) / 1e9, 1e18); + assertApproxEqRel(issuedFromYieldBearingAsset, (totalIssued * targetExposure) / 1e9, 1e18); + + (uint8 increase, uint256 amount) = harvester.computeRebalanceAmount(USDM); + assertEq(increase, 1); // There is still a small amount to mint because of the transmuter fees and slippage } function test_harvest_DecreaseExposureUSDM(uint256 amount) external { - _loadReserve(USDC, 1e11); - _loadReserve(USDM, 1e8); + amount = bound(amount, 1e15, 1e23); + _loadReserve(USDM, amount); _setYieldBearingData(USDM, USDC); + (uint256 issuedFromYieldBearingAssetBefore, ) = transmuter.getIssuedByCollateral(USDM); + (uint256 issuedFromStablecoinBefore, uint256 totalIssuedBefore) = transmuter.getIssuedByCollateral(USDC); + (uint8 expectedIncrease, uint256 expectedAmount) = harvester.computeRebalanceAmount(USDM); + + assertEq(expectedIncrease, 0); + assertEq(issuedFromStablecoinBefore, 0); + assertEq(issuedFromYieldBearingAssetBefore, amount); + assertEq(totalIssuedBefore, issuedFromYieldBearingAssetBefore); + assertEq(expectedAmount, issuedFromYieldBearingAssetBefore - ((targetExposure * totalIssuedBefore) / 1e9)); + vm.prank(alice); harvester.harvest(USDM, 1e9, new bytes(0)); + + assertEq(IERC20(USDC).balanceOf(address(harvester)), 0); + assertEq(IERC20(USDM).balanceOf(address(harvester)), 0); + assertApproxEqRel(IERC20(USDM).balanceOf(address(receiver)), expectedAmount, 1e18); + assertEq(agToken.balanceOf(address(harvester)), 0); + + // fake semd USDC to harvester + deal(USDC, address(harvester), amount); + + vm.prank(alice); + harvester.finalizeRebalance(USDC, amount); + + assertEq(agToken.balanceOf(address(harvester)), 0); + assertEq(IERC20(USDC).balanceOf(address(harvester)), 0); + + (uint256 issuedFromYieldBearingAsset, uint256 totalIssued) = transmuter.getIssuedByCollateral(USDM); + (uint256 issuedFromStablecoin, ) = transmuter.getIssuedByCollateral(USDC); + assertEq(totalIssued, issuedFromStablecoin + issuedFromYieldBearingAsset); + assertApproxEqRel(issuedFromStablecoin, (totalIssued * (1e9 - targetExposure)) / 1e9, 1e18); + assertApproxEqRel(issuedFromYieldBearingAsset, (totalIssued * targetExposure) / 1e9, 1e18); + + (uint8 increase, uint256 amount) = harvester.computeRebalanceAmount(USDM); + assertEq(increase, 1); // There is still a small amount to mint because of the transmuter fees and slippage } - function test_FinalizeRebalance_SlippageTooHigh(uint256 amount) external { + function test_ComputeRebalanceAmount_HigherThanMax() external { + _loadReserve(XEVT, 1e11); + _loadReserve(EURC, 1e11); + uint64 minExposure = uint64((15 * 1e9) / 100); + uint64 maxExposure = uint64((40 * 1e9) / 100); + _setYieldBearingData(XEVT, EURC, minExposure, maxExposure); + + (uint8 increase, uint256 amount) = harvester.computeRebalanceAmount(XEVT); + assertEq(amount, 0); + assertEq(increase, 0); + } + + function test_ComputeRebalanceAmount_HigherThanMaxWithHarvest() external { + _loadReserve(XEVT, 1e11); + _loadReserve(EURC, 1e11); + uint64 minExposure = uint64((15 * 1e9) / 100); + uint64 maxExposure = uint64((60 * 1e9) / 100); + _setYieldBearingData(XEVT, EURC, minExposure, maxExposure); + + (uint8 increase, uint256 amount) = harvester.computeRebalanceAmount(XEVT); + assertEq(amount, (2e23 * uint256(maxExposure)) / 1e9 - 1e23); + assertEq(increase, 0); + } + + function test_ComputeRebalanceAmount_LowerThanMin() external { + _loadReserve(EURC, 9e10); + _loadReserve(XEVT, 1e10); + uint64 minExposure = uint64((99 * 1e9) / 100); + uint64 maxExposure = uint64((999 * 1e9) / 1000); + _setYieldBearingData(XEVT, EURC, minExposure, maxExposure); + + (uint8 increase, uint256 amount) = harvester.computeRebalanceAmount(XEVT); + assertEq(amount, 0); + assertEq(increase, 1); + } + + function test_ComputeRebalanceAmount_LowerThanMinAfterHarvest() external { + _loadReserve(EURC, 9e10); + _loadReserve(XEVT, 1e10); + uint64 minExposure = uint64((89 * 1e9) / 100); + uint64 maxExposure = uint64((999 * 1e9) / 1000); + _setYieldBearingData(XEVT, EURC, minExposure, maxExposure); + + (uint8 increase, uint256 amount) = harvester.computeRebalanceAmount(XEVT); + assertEq(amount, 9e22 - (1e23 * uint256(minExposure)) / 1e9); + assertEq(increase, 1); + } + + function test_SlippageTooHighStablecoin(uint256 amount) external { // TODO } @@ -410,6 +556,16 @@ contract MultiBlockHarvestertTest is Fixture, FunctionUtils { 1 ); } + + function _setYieldBearingData( + address yieldBearingAsset, + address stablecoin, + uint64 minExposure, + uint64 maxExposure + ) internal { + vm.prank(governor); + harvester.setYieldBearingAssetData(yieldBearingAsset, stablecoin, targetExposure, minExposure, maxExposure, 1); + } } interface Wow { From 3fc3e8359a2cd18d3881b3e77e7eefdda3f436cb Mon Sep 17 00:00:00 2001 From: 0xtekgrinder <0xtekgrinder@protonmail.com> Date: Tue, 8 Oct 2024 17:15:25 +0200 Subject: [PATCH 30/69] refactor: rename minExposure / maxExposure to be more explicit --- contracts/helpers/BaseHarvester.sol | 38 ++++++++++++++--------------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/contracts/helpers/BaseHarvester.sol b/contracts/helpers/BaseHarvester.sol index 0941b4e4..8095b33e 100644 --- a/contracts/helpers/BaseHarvester.sol +++ b/contracts/helpers/BaseHarvester.sol @@ -17,9 +17,9 @@ struct YieldBearingParams { // Target exposure to the collateral yield bearing asset used uint64 targetExposure; // Maximum exposure within the Transmuter to the stablecoin asset - uint64 maxExposureYieldAsset; + uint64 maxExposure; // Minimum exposure within the Transmuter to the stablecoin asset - uint64 minExposureYieldAsset; + uint64 minExposure; // 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; @@ -69,24 +69,24 @@ abstract contract BaseHarvester is IHarvester, AccessControl { * @param yieldBearingAsset address of the yieldBearingAsset * @param stablecoin address of the stablecoin * @param targetExposure target exposure to the yieldBearingAsset asset used - * @param minExposureYieldAsset minimum exposure within the Transmuter to the stablecoin - * @param maxExposureYieldAsset maximum exposure within the Transmuter to the stablecoin + * @param minExposure minimum exposure within the Transmuter to the stablecoin + * @param maxExposure maximum exposure within the Transmuter to the stablecoin * @param overrideExposures whether limit exposures should be overriden or read onchain through the Transmuter */ function setYieldBearingAssetData( address yieldBearingAsset, address stablecoin, uint64 targetExposure, - uint64 minExposureYieldAsset, - uint64 maxExposureYieldAsset, + uint64 minExposure, + uint64 maxExposure, uint64 overrideExposures ) external onlyGuardian { _setYieldBearingAssetData( yieldBearingAsset, stablecoin, targetExposure, - minExposureYieldAsset, - maxExposureYieldAsset, + minExposure, + maxExposure, overrideExposures ); } @@ -139,7 +139,7 @@ abstract contract BaseHarvester is IHarvester, AccessControl { if (stablecoinsFromYieldBearingAsset * 1e9 > targetExposureScaled) { // Need to decrease exposure to yield bearing asset amount = stablecoinsFromYieldBearingAsset - targetExposureScaled / 1e9; - uint256 maxValueScaled = yieldBearingInfo.maxExposureYieldAsset * stablecoinsIssued; + uint256 maxValueScaled = yieldBearingInfo.maxExposure * 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 (stablecoinsFromStablecoin * 1e9 > maxValueScaled) amount = 0; @@ -150,7 +150,7 @@ abstract contract BaseHarvester is IHarvester, AccessControl { // collateral may be obtained by burning stablecoins for the yield asset and unwrapping it increase = 1; amount = targetExposureScaled / 1e9 - stablecoinsFromYieldBearingAsset; - uint256 minValueScaled = yieldBearingInfo.minExposureYieldAsset * stablecoinsIssued; + uint256 minValueScaled = yieldBearingInfo.minExposure * stablecoinsIssued; if (stablecoinsFromStablecoin * 1e9 < minValueScaled) amount = 0; else if (stablecoinsFromStablecoin * 1e9 < minValueScaled + amount * 1e9) amount = stablecoinsFromStablecoin - minValueScaled / 1e9; @@ -161,8 +161,8 @@ abstract contract BaseHarvester is IHarvester, AccessControl { address yieldBearingAsset, address stablecoin, uint64 targetExposure, - uint64 minExposureYieldAsset, - uint64 maxExposureYieldAsset, + uint64 minExposure, + uint64 maxExposure, uint64 overrideExposures ) internal virtual { YieldBearingParams storage yieldBearingInfo = yieldBearingData[yieldBearingAsset]; @@ -171,9 +171,9 @@ abstract contract BaseHarvester is IHarvester, AccessControl { yieldBearingInfo.targetExposure = targetExposure; yieldBearingInfo.overrideExposures = overrideExposures; if (overrideExposures == 1) { - if (maxExposureYieldAsset >= 1e9 || minExposureYieldAsset >= maxExposureYieldAsset) revert InvalidParam(); - yieldBearingInfo.maxExposureYieldAsset = maxExposureYieldAsset; - yieldBearingInfo.minExposureYieldAsset = minExposureYieldAsset; + if (maxExposure >= 1e9 || minExposure >= maxExposure) revert InvalidParam(); + yieldBearingInfo.maxExposure = maxExposure; + yieldBearingInfo.minExposure = minExposure; } else { yieldBearingInfo.overrideExposures = 2; _updateLimitExposuresYieldAsset(yieldBearingInfo.stablecoin, yieldBearingInfo); @@ -187,14 +187,14 @@ abstract contract BaseHarvester is IHarvester, AccessControl { uint64[] memory xFeeMint; (xFeeMint, ) = transmuter.getCollateralMintFees(stablecoin); uint256 length = xFeeMint.length; - if (length <= 1) yieldBearingInfo.maxExposureYieldAsset = 1e9; - else yieldBearingInfo.maxExposureYieldAsset = xFeeMint[length - 2]; + if (length <= 1) yieldBearingInfo.maxExposure = 1e9; + else yieldBearingInfo.maxExposure = xFeeMint[length - 2]; uint64[] memory xFeeBurn; (xFeeBurn, ) = transmuter.getCollateralBurnFees(stablecoin); length = xFeeBurn.length; - if (length <= 1) yieldBearingInfo.minExposureYieldAsset = 0; - else yieldBearingInfo.minExposureYieldAsset = xFeeBurn[length - 2]; + if (length <= 1) yieldBearingInfo.minExposure = 0; + else yieldBearingInfo.minExposure = xFeeBurn[length - 2]; } function _setMaxSlippage(uint96 newMaxSlippage) internal virtual { From 378f35f4fdb4d98aaeb2aaf52d9102ed287e94fa Mon Sep 17 00:00:00 2001 From: 0xtekgrinder <0xtekgrinder@protonmail.com> Date: Tue, 15 Oct 2024 18:52:08 +0200 Subject: [PATCH 31/69] feat: check for msg.sender instead of receiver when removing budget --- contracts/helpers/GenericHarvester.sol | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/contracts/helpers/GenericHarvester.sol b/contracts/helpers/GenericHarvester.sol index 9e17321f..27a1b995 100644 --- a/contracts/helpers/GenericHarvester.sol +++ b/contracts/helpers/GenericHarvester.sol @@ -170,8 +170,8 @@ contract GenericHarvester is BaseHarvester, IERC3156FlashBorrower, RouterSwapper * @param receiver address of the receiver */ function removeBudget(uint256 amount, address receiver) public virtual { - if (budget[receiver] < amount) revert InsufficientFunds(); - budget[receiver] -= amount; + if (budget[msg.sender] < amount) revert InsufficientFunds(); + budget[msg.sender] -= amount; IERC20(agToken).safeTransfer(receiver, amount); } From 35132a8975225899c2858e9ccd688133851597a5 Mon Sep 17 00:00:00 2001 From: 0xtekgrinder <0xtekgrinder@protonmail.com> Date: Tue, 15 Oct 2024 18:54:13 +0200 Subject: [PATCH 32/69] doc: add some explanation on scale --- contracts/helpers/GenericHarvester.sol | 2 ++ 1 file changed, 2 insertions(+) diff --git a/contracts/helpers/GenericHarvester.sol b/contracts/helpers/GenericHarvester.sol index 27a1b995..b9247b07 100644 --- a/contracts/helpers/GenericHarvester.sol +++ b/contracts/helpers/GenericHarvester.sol @@ -257,6 +257,8 @@ contract GenericHarvester is BaseHarvester, IERC3156FlashBorrower, RouterSwapper /// some yieldBearingAsset 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 /// `yieldBearingAsset` to the target exposure + /// @dev scale is a number between 0 and 1e9 that represents the proportion of the + /// it is used to lower the amount of the asset to harvest for exemple to have a lower slippage function harvest(address yieldBearingAsset, uint256 scale, bytes calldata extraData) public virtual { if (scale > 1e9) revert InvalidParam(); YieldBearingParams memory yieldBearingInfo = yieldBearingData[yieldBearingAsset]; From 8abc89338f545c117d7a7ad446dc1732097b972e Mon Sep 17 00:00:00 2001 From: 0xtekgrinder <0xtekgrinder@protonmail.com> Date: Mon, 21 Oct 2024 09:52:28 +0200 Subject: [PATCH 33/69] feat: system for allowed addresses to update the target exposure of a yield bearing asset --- contracts/helpers/BaseHarvester.sol | 32 +++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/contracts/helpers/BaseHarvester.sol b/contracts/helpers/BaseHarvester.sol index 8095b33e..510fdcd6 100644 --- a/contracts/helpers/BaseHarvester.sol +++ b/contracts/helpers/BaseHarvester.sol @@ -31,6 +31,15 @@ struct YieldBearingParams { abstract contract BaseHarvester is IHarvester, AccessControl { using SafeERC20 for IERC20; + /*////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + MODIFIERS + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////*/ + + modifier onlyAllowed() { + if (!isAllowed[msg.sender]) revert NotTrusted(); + _; + } + /*////////////////////////////////////////////////////////////////////////////////////////////////////////////////// VARIABLES //////////////////////////////////////////////////////////////////////////////////////////////////////////////////*/ @@ -43,6 +52,8 @@ abstract contract BaseHarvester is IHarvester, AccessControl { uint96 public maxSlippage; /// @notice Data associated to a yield bearing asset mapping(address => YieldBearingParams) public yieldBearingData; + /// @notice Whether an address is allowed to update the target exposure of a yield bearing asset + mapping(address => bool) public isAllowed; /*////////////////////////////////////////////////////////////////////////////////////////////////////////////////// CONSTRUCTOR @@ -109,6 +120,27 @@ abstract contract BaseHarvester is IHarvester, AccessControl { _setMaxSlippage(newMaxSlippage); } + /** + * @notice add an address to the allowed list + * @param account address to be added + */ + function toggleAllowed(address account) external onlyGuardian { + isAllowed[account] = !isAllowed[account]; + } + + /*////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + ALLOWED FUNCTIONS + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////*/ + + /** + * @notice Set the target exposure of a yield bearing asset + * @param yieldBearingAsset address of the yield bearing asset + * @param targetExposure target exposure to the yield bearing asset used + */ + function setTargetExposure(address yieldBearingAsset, uint64 targetExposure) external onlyAllowed { + yieldBearingData[yieldBearingAsset].targetExposure = targetExposure; + } + /*////////////////////////////////////////////////////////////////////////////////////////////////////////////////// VIEW FUNCTIONS //////////////////////////////////////////////////////////////////////////////////////////////////////////////////*/ From 7c417ff815269982700f9b263812d8c2f0d464f6 Mon Sep 17 00:00:00 2001 From: 0xtekgrinder <0xtekgrinder@protonmail.com> Date: Mon, 21 Oct 2024 10:52:42 +0200 Subject: [PATCH 34/69] doc: finalize sentance --- contracts/helpers/GenericHarvester.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/helpers/GenericHarvester.sol b/contracts/helpers/GenericHarvester.sol index b9247b07..16f4a6d6 100644 --- a/contracts/helpers/GenericHarvester.sol +++ b/contracts/helpers/GenericHarvester.sol @@ -257,7 +257,7 @@ contract GenericHarvester is BaseHarvester, IERC3156FlashBorrower, RouterSwapper /// some yieldBearingAsset 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 /// `yieldBearingAsset` to the target exposure - /// @dev scale is a number between 0 and 1e9 that represents the proportion of the + /// @dev scale is a number between 0 and 1e9 that represents the proportion of the agToken to harvest, /// it is used to lower the amount of the asset to harvest for exemple to have a lower slippage function harvest(address yieldBearingAsset, uint256 scale, bytes calldata extraData) public virtual { if (scale > 1e9) revert InvalidParam(); From 3e11a2b19d88c6b1cc669dd744be46295b024227 Mon Sep 17 00:00:00 2001 From: 0xtekgrinder <0xtekgrinder@protonmail.com> Date: Mon, 21 Oct 2024 10:53:41 +0200 Subject: [PATCH 35/69] doc: misspell --- contracts/helpers/GenericHarvester.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/helpers/GenericHarvester.sol b/contracts/helpers/GenericHarvester.sol index 16f4a6d6..01fd2252 100644 --- a/contracts/helpers/GenericHarvester.sol +++ b/contracts/helpers/GenericHarvester.sol @@ -258,7 +258,7 @@ contract GenericHarvester is BaseHarvester, IERC3156FlashBorrower, RouterSwapper /// @dev Due to potential transaction fees within the Transmuter, this function doesn't exactly bring /// `yieldBearingAsset` to the target exposure /// @dev scale is a number between 0 and 1e9 that represents the proportion of the agToken to harvest, - /// it is used to lower the amount of the asset to harvest for exemple to have a lower slippage + /// it is used to lower the amount of the asset to harvest for example to have a lower slippage function harvest(address yieldBearingAsset, uint256 scale, bytes calldata extraData) public virtual { if (scale > 1e9) revert InvalidParam(); YieldBearingParams memory yieldBearingInfo = yieldBearingData[yieldBearingAsset]; From 9a10225c92a08d7cb2a9899720ade21d9546c0b3 Mon Sep 17 00:00:00 2001 From: 0xtekgrinder <0xtekgrinder@protonmail.com> Date: Mon, 21 Oct 2024 14:56:20 +0200 Subject: [PATCH 36/69] feat: remove underflow checks --- contracts/helpers/GenericHarvester.sol | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/contracts/helpers/GenericHarvester.sol b/contracts/helpers/GenericHarvester.sol index 01fd2252..4563dc3f 100644 --- a/contracts/helpers/GenericHarvester.sol +++ b/contracts/helpers/GenericHarvester.sol @@ -147,8 +147,7 @@ contract GenericHarvester is BaseHarvester, IERC3156FlashBorrower, RouterSwapper block.timestamp ); if (amount > amountStableOut) { - if (budget[sender] < amount - amountStableOut) revert InsufficientFunds(); - budget[sender] -= amount - amountStableOut; + budget[sender] -= amount - amountStableOut; // Will revert if not enough funds } return CALLBACK_SUCCESS; } @@ -170,8 +169,7 @@ contract GenericHarvester is BaseHarvester, IERC3156FlashBorrower, RouterSwapper * @param receiver address of the receiver */ function removeBudget(uint256 amount, address receiver) public virtual { - if (budget[msg.sender] < amount) revert InsufficientFunds(); - budget[msg.sender] -= amount; + budget[msg.sender] -= amount; // Will revert if not enough funds IERC20(agToken).safeTransfer(receiver, amount); } From 427eaf5eff047cce1a1772043d78d9eed9ae48e5 Mon Sep 17 00:00:00 2001 From: 0xtekgrinder <0xtekgrinder@protonmail.com> Date: Mon, 21 Oct 2024 17:00:33 +0200 Subject: [PATCH 37/69] feat: make updateLimitExposure permissionless --- contracts/helpers/BaseHarvester.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/helpers/BaseHarvester.sol b/contracts/helpers/BaseHarvester.sol index 510fdcd6..82df1137 100644 --- a/contracts/helpers/BaseHarvester.sol +++ b/contracts/helpers/BaseHarvester.sol @@ -106,7 +106,7 @@ abstract contract BaseHarvester is IHarvester, AccessControl { * @notice Set the limit exposures to the stablecoin linked to the yield bearing asset * @param yieldBearingAsset address of the yield bearing asset */ - function updateLimitExposuresYieldAsset(address yieldBearingAsset) public virtual onlyGuardian { + function updateLimitExposuresYieldAsset(address yieldBearingAsset) public virtual { YieldBearingParams storage yieldBearingInfo = yieldBearingData[yieldBearingAsset]; if (yieldBearingInfo.overrideExposures == 2) _updateLimitExposuresYieldAsset(yieldBearingInfo.stablecoin, yieldBearingInfo); From dea0f6c56775d1d768de61745aa7d1577b43e886 Mon Sep 17 00:00:00 2001 From: 0xtekgrinder <0xtekgrinder@protonmail.com> Date: Mon, 21 Oct 2024 17:10:19 +0200 Subject: [PATCH 38/69] feat: internalize scaling of amount --- contracts/helpers/BaseHarvester.sol | 22 ++++++++++++++++++++++ contracts/helpers/GenericHarvester.sol | 6 +----- contracts/helpers/MultiBlockHarvester.sol | 14 +------------- 3 files changed, 24 insertions(+), 18 deletions(-) diff --git a/contracts/helpers/BaseHarvester.sol b/contracts/helpers/BaseHarvester.sol index 82df1137..4a8c44c9 100644 --- a/contracts/helpers/BaseHarvester.sol +++ b/contracts/helpers/BaseHarvester.sol @@ -234,6 +234,28 @@ abstract contract BaseHarvester is IHarvester, AccessControl { maxSlippage = newMaxSlippage; } + function _scaleAmountBasedOnDecimals( + uint256 decimalsTokenIn, + uint256 decimalsTokenOut, + uint256 amountIn, + bool assetIn + ) internal pure returns (uint256) { + if (decimalsTokenIn > decimalsTokenOut) { + if (assetIn) { + amountIn /= 10 ** (decimalsTokenIn - decimalsTokenOut); + } else { + amountIn *= 10 ** (decimalsTokenIn - decimalsTokenOut); + } + } else if (decimalsTokenIn < decimalsTokenOut) { + if (assetIn) { + amountIn *= 10 ** (decimalsTokenOut - decimalsTokenIn); + } else { + amountIn /= 10 ** (decimalsTokenOut - decimalsTokenIn); + } + } + return amountIn; + } + /*////////////////////////////////////////////////////////////////////////////////////////////////////////////////// HELPER //////////////////////////////////////////////////////////////////////////////////////////////////////////////////*/ diff --git a/contracts/helpers/GenericHarvester.sol b/contracts/helpers/GenericHarvester.sol index 4563dc3f..7a7fc92f 100644 --- a/contracts/helpers/GenericHarvester.sol +++ b/contracts/helpers/GenericHarvester.sol @@ -216,11 +216,7 @@ contract GenericHarvester is BaseHarvester, IERC3156FlashBorrower, RouterSwapper uint256 decimalsTokenIn = IERC20Metadata(tokenIn).decimals(); uint256 decimalsTokenOut = IERC20Metadata(tokenOut).decimals(); - if (decimalsTokenIn > decimalsTokenOut) { - amount /= 10 ** (decimalsTokenIn - decimalsTokenOut); - } else if (decimalsTokenIn < decimalsTokenOut) { - amount *= 10 ** (decimalsTokenOut - decimalsTokenIn); - } + amount = _scaleAmountBasedOnDecimals(decimalsTokenIn, decimalsTokenOut, amount, true); if (amountOut < (amount * (BPS - maxSwapSlippage)) / BPS) { revert SlippageTooHigh(); } diff --git a/contracts/helpers/MultiBlockHarvester.sol b/contracts/helpers/MultiBlockHarvester.sol index d3ffe83e..c8dd0f4a 100644 --- a/contracts/helpers/MultiBlockHarvester.sol +++ b/contracts/helpers/MultiBlockHarvester.sol @@ -217,19 +217,7 @@ contract MultiBlockHarvester is BaseHarvester { uint256 decimalsAsset = IERC20Metadata(asset).decimals(); // Divide or multiply the amountIn to match the decimals of the asset - if (decimalsAsset > 18) { - if (assetIn) { - amountIn /= 10 ** (decimalsAsset - 18); - } else { - amountIn *= 10 ** (decimalsAsset - 18); - } - } else if (decimalsAsset < 18) { - if (assetIn) { - amountIn *= 10 ** (18 - decimalsAsset); - } else { - amountIn /= 10 ** (18 - decimalsAsset); - } - } + amountIn = _scaleAmountBasedOnDecimals(decimalsAsset, 18, amountIn, assetIn); if (asset == USDC || asset == USDM || asset == EURC) { // Assume 1:1 ratio between stablecoins From 4aa4cdc6bc13106e6e2e6ba5f17cdb8672953055 Mon Sep 17 00:00:00 2001 From: 0xtekgrinder <0xtekgrinder@protonmail.com> Date: Mon, 21 Oct 2024 17:16:10 +0200 Subject: [PATCH 39/69] feat: only allowed or guardian --- contracts/helpers/BaseHarvester.sol | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/contracts/helpers/BaseHarvester.sol b/contracts/helpers/BaseHarvester.sol index 4a8c44c9..ab5c314d 100644 --- a/contracts/helpers/BaseHarvester.sol +++ b/contracts/helpers/BaseHarvester.sol @@ -35,8 +35,8 @@ abstract contract BaseHarvester is IHarvester, AccessControl { MODIFIERS //////////////////////////////////////////////////////////////////////////////////////////////////////////////////*/ - modifier onlyAllowed() { - if (!isAllowed[msg.sender]) revert NotTrusted(); + modifier onlyAllowedOrGuardian() { + if (!isAllowed[msg.sender] && !accessControlManager.isGovernorOrGuardian(msg.sender)) revert NotTrusted(); _; } @@ -137,7 +137,7 @@ abstract contract BaseHarvester is IHarvester, AccessControl { * @param yieldBearingAsset address of the yield bearing asset * @param targetExposure target exposure to the yield bearing asset used */ - function setTargetExposure(address yieldBearingAsset, uint64 targetExposure) external onlyAllowed { + function setTargetExposure(address yieldBearingAsset, uint64 targetExposure) external onlyAllowedOrGuardian { yieldBearingData[yieldBearingAsset].targetExposure = targetExposure; } From 0ba62151a117cb390a1e3fa1d012425b16fca695 Mon Sep 17 00:00:00 2001 From: 0xtekgrinder <0xtekgrinder@protonmail.com> Date: Tue, 22 Oct 2024 11:05:41 +0200 Subject: [PATCH 40/69] fix: update state before sending tokens in addBudget --- contracts/helpers/GenericHarvester.sol | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/contracts/helpers/GenericHarvester.sol b/contracts/helpers/GenericHarvester.sol index 7a7fc92f..943a2e0e 100644 --- a/contracts/helpers/GenericHarvester.sol +++ b/contracts/helpers/GenericHarvester.sol @@ -158,9 +158,9 @@ contract GenericHarvester is BaseHarvester, IERC3156FlashBorrower, RouterSwapper * @param receiver address of the receiver */ function addBudget(uint256 amount, address receiver) public virtual { - IERC20(agToken).safeTransferFrom(msg.sender, address(this), amount); - budget[receiver] += amount; + + IERC20(agToken).safeTransferFrom(msg.sender, address(this), amount); } /** From 4b701a8ab70b443766bd08b7597147b0be2cc6cd Mon Sep 17 00:00:00 2001 From: 0xtekgrinder <0xtekgrinder@protonmail.com> Date: Tue, 22 Oct 2024 17:38:27 +0200 Subject: [PATCH 41/69] style: refactor blocks for GenericHarvester --- contracts/helpers/GenericHarvester.sol | 158 +++++++++++++------------ 1 file changed, 81 insertions(+), 77 deletions(-) diff --git a/contracts/helpers/GenericHarvester.sol b/contracts/helpers/GenericHarvester.sol index 943a2e0e..f5c8d7ce 100644 --- a/contracts/helpers/GenericHarvester.sol +++ b/contracts/helpers/GenericHarvester.sol @@ -66,9 +66,66 @@ contract GenericHarvester is BaseHarvester, IERC3156FlashBorrower, RouterSwapper } /*////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - REBALANCE + BUDGET HANDLING //////////////////////////////////////////////////////////////////////////////////////////////////////////////////*/ + /** + * @notice Add budget to be spent by the receiver during the flashloan + * @param amount amount of AGToken to add to the budget + * @param receiver address of the receiver + */ + function addBudget(uint256 amount, address receiver) public virtual { + budget[receiver] += amount; + + IERC20(agToken).safeTransferFrom(msg.sender, address(this), amount); + } + + /** + * @notice Remove budget from the owner and send it to the receiver + * @param amount amount of AGToken to remove from the budget + * @param receiver address of the receiver + */ + function removeBudget(uint256 amount, address receiver) public virtual { + budget[msg.sender] -= amount; // Will revert if not enough funds + + IERC20(agToken).safeTransfer(receiver, amount); + } + + /*////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + HARVEST + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////*/ + + /// @notice Invests or divests from the yield asset associated to `yieldBearingAsset` based on the current exposure + /// to this yieldBearingAsset + /// @dev This transaction either reduces the exposure to `yieldBearingAsset` in the Transmuter or frees up + /// some yieldBearingAsset 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 + /// `yieldBearingAsset` to the target exposure + /// @dev scale is a number between 0 and 1e9 that represents the proportion of the agToken to harvest, + /// it is used to lower the amount of the asset to harvest for example to have a lower slippage + function harvest(address yieldBearingAsset, uint256 scale, bytes calldata extraData) public virtual { + if (scale > 1e9) revert InvalidParam(); + YieldBearingParams memory yieldBearingInfo = yieldBearingData[yieldBearingAsset]; + (uint8 increase, uint256 amount) = _computeRebalanceAmount(yieldBearingAsset, yieldBearingInfo); + amount = (amount * scale) / 1e9; + + (SwapType swapType, bytes memory data) = abi.decode(extraData, (SwapType, bytes)); + + if (amount > 0) { + try transmuter.updateOracle(yieldBearingInfo.stablecoin) {} catch {} + + adjustYieldExposure( + amount, + increase, + yieldBearingAsset, + yieldBearingInfo.stablecoin, + (amount * (1e9 - maxSlippage)) / 1e9, + swapType, + data + ); + } + } + /// @notice Burns `amountStablecoins` for one yieldBearing 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 @@ -152,28 +209,38 @@ contract GenericHarvester is BaseHarvester, IERC3156FlashBorrower, RouterSwapper return CALLBACK_SUCCESS; } + /*////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + SETTERS + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////*/ + /** - * @notice Add budget to a receiver - * @param amount amount of AGToken to add to the budget - * @param receiver address of the receiver + * @notice Set the token transfer address + * @param newTokenTransferAddress address of the token transfer contract */ - function addBudget(uint256 amount, address receiver) public virtual { - budget[receiver] += amount; - - IERC20(agToken).safeTransferFrom(msg.sender, address(this), amount); + function setTokenTransferAddress(address newTokenTransferAddress) public override onlyGuardian { + super.setTokenTransferAddress(newTokenTransferAddress); } /** - * @notice Remove budget from a receiver - * @param amount amount of AGToken to remove from the budget - * @param receiver address of the receiver + * @notice Set the swap router + * @param newSwapRouter address of the swap router */ - function removeBudget(uint256 amount, address receiver) public virtual { - budget[msg.sender] -= amount; // Will revert if not enough funds + function setSwapRouter(address newSwapRouter) public override onlyGuardian { + super.setSwapRouter(newSwapRouter); + } - IERC20(agToken).safeTransfer(receiver, amount); + /** + * @notice Set the max swap slippage + * @param newMaxSwapSlippage max slippage in BPS + */ + function setMaxSwapSlippage(uint32 newMaxSwapSlippage) external onlyGuardian { + maxSwapSlippage = newMaxSwapSlippage; } + /*////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + INTERNALS + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////*/ + function _swapToTokenOut( uint256 typeAction, address tokenIn, @@ -240,67 +307,4 @@ contract GenericHarvester is BaseHarvester, IERC3156FlashBorrower, RouterSwapper amountOut = IERC4626(tokenOut).deposit(amount, address(this)); } else amountOut = IERC4626(tokenOut).redeem(amount, address(this), address(this)); } - - /*////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - HARVEST - //////////////////////////////////////////////////////////////////////////////////////////////////////////////////*/ - - /// @notice Invests or divests from the yield asset associated to `yieldBearingAsset` based on the current exposure - /// to this yieldBearingAsset - /// @dev This transaction either reduces the exposure to `yieldBearingAsset` in the Transmuter or frees up - /// some yieldBearingAsset 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 - /// `yieldBearingAsset` to the target exposure - /// @dev scale is a number between 0 and 1e9 that represents the proportion of the agToken to harvest, - /// it is used to lower the amount of the asset to harvest for example to have a lower slippage - function harvest(address yieldBearingAsset, uint256 scale, bytes calldata extraData) public virtual { - if (scale > 1e9) revert InvalidParam(); - YieldBearingParams memory yieldBearingInfo = yieldBearingData[yieldBearingAsset]; - (uint8 increase, uint256 amount) = _computeRebalanceAmount(yieldBearingAsset, yieldBearingInfo); - amount = (amount * scale) / 1e9; - - (SwapType swapType, bytes memory data) = abi.decode(extraData, (SwapType, bytes)); - - if (amount > 0) { - try transmuter.updateOracle(yieldBearingInfo.stablecoin) {} catch {} - - adjustYieldExposure( - amount, - increase, - yieldBearingAsset, - yieldBearingInfo.stablecoin, - (amount * (1e9 - maxSlippage)) / 1e9, - swapType, - data - ); - } - } - - /*////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - SETTERS - //////////////////////////////////////////////////////////////////////////////////////////////////////////////////*/ - - /** - * @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 swap slippage - * @param newMaxSwapSlippage max slippage in BPS - */ - function setMaxSwapSlippage(uint32 newMaxSwapSlippage) external onlyGuardian { - maxSwapSlippage = newMaxSwapSlippage; - } } From e1c67a75c5d714057b3ed9ee249ed1cdfa63946c Mon Sep 17 00:00:00 2001 From: 0xtekgrinder <0xtekgrinder@protonmail.com> Date: Tue, 22 Oct 2024 17:52:57 +0200 Subject: [PATCH 42/69] fix: better slippage handling for vaults --- contracts/helpers/GenericHarvester.sol | 26 ++++++++++++++------------ 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/contracts/helpers/GenericHarvester.sol b/contracts/helpers/GenericHarvester.sol index f5c8d7ce..834c6292 100644 --- a/contracts/helpers/GenericHarvester.sol +++ b/contracts/helpers/GenericHarvester.sol @@ -252,7 +252,15 @@ contract GenericHarvester is BaseHarvester, IERC3156FlashBorrower, RouterSwapper if (swapType == SwapType.SWAP) { amountOut = _swapToTokenOutSwap(tokenIn, tokenOut, amount, callData); } else if (swapType == SwapType.VAULT) { - amountOut = _swapToTokenOutVault(typeAction, tokenOut, amount); + amountOut = _swapToTokenOutVault(typeAction, tokenIn, tokenOut, amount); + } + + // Check for slippage + uint256 decimalsTokenIn = IERC20Metadata(tokenIn).decimals(); + uint256 decimalsTokenOut = IERC20Metadata(tokenOut).decimals(); + amount = _scaleAmountBasedOnDecimals(decimalsTokenIn, decimalsTokenOut, amount, true); + if (amountOut < (amount * (BPS - maxSwapSlippage)) / BPS) { + revert SlippageTooHigh(); } } @@ -279,32 +287,26 @@ contract GenericHarvester is BaseHarvester, IERC3156FlashBorrower, RouterSwapper amounts[0] = amount; _swap(tokens, callDatas, amounts); - uint256 amountOut = IERC20(tokenOut).balanceOf(address(this)) - balance; - uint256 decimalsTokenIn = IERC20Metadata(tokenIn).decimals(); - uint256 decimalsTokenOut = IERC20Metadata(tokenOut).decimals(); - - amount = _scaleAmountBasedOnDecimals(decimalsTokenIn, decimalsTokenOut, amount, true); - if (amountOut < (amount * (BPS - maxSwapSlippage)) / BPS) { - revert SlippageTooHigh(); - } - return amountOut; + return IERC20(tokenOut).balanceOf(address(this)) - balance; } /** * @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 _swapToTokenOutVault( uint256 typeAction, + address tokenIn, address tokenOut, uint256 amount ) internal returns (uint256 amountOut) { if (typeAction == 1) { // Granting allowance with the yieldBearingAsset for the vault asset - _adjustAllowance(tokenOut, tokenOut, amount); - amountOut = IERC4626(tokenOut).deposit(amount, address(this)); + _adjustAllowance(tokenOut, tokenIn, amount); + amountOut = IERC4626(tokenIn).deposit(amount, address(this)); } else amountOut = IERC4626(tokenOut).redeem(amount, address(this), address(this)); } } From f9e9a5f4ee7a564d62e71c1cc9bf1b1a3118677f Mon Sep 17 00:00:00 2001 From: 0xtekgrinder <0xtekgrinder@protonmail.com> Date: Tue, 22 Oct 2024 18:34:38 +0200 Subject: [PATCH 43/69] feat: IXEVT interface --- contracts/interfaces/IXEVT.sol | 6 ++++++ test/fuzz/MultiBlockHarvester.t.sol | 8 +++----- 2 files changed, 9 insertions(+), 5 deletions(-) create mode 100644 contracts/interfaces/IXEVT.sol diff --git a/contracts/interfaces/IXEVT.sol b/contracts/interfaces/IXEVT.sol new file mode 100644 index 00000000..737c3927 --- /dev/null +++ b/contracts/interfaces/IXEVT.sol @@ -0,0 +1,6 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.19; + +interface IXEVT { + function isAllowed(address) external returns (bool); +} diff --git a/test/fuzz/MultiBlockHarvester.t.sol b/test/fuzz/MultiBlockHarvester.t.sol index e0a0d600..7eed9a38 100644 --- a/test/fuzz/MultiBlockHarvester.t.sol +++ b/test/fuzz/MultiBlockHarvester.t.sol @@ -17,6 +17,8 @@ import "contracts/helpers/MultiBlockHarvester.sol"; import "contracts/transmuter/Storage.sol"; +import { IXEVT } from "interfaces/IXEVT.sol"; + import { IERC4626 } from "oz/token/ERC20/extensions/ERC4626.sol"; import { IAccessControl } from "oz/access/IAccessControl.sol"; @@ -151,7 +153,7 @@ contract MultiBlockHarvestertTest is Fixture, FunctionUtils { // mock isAllowed(address) returns (bool) to transfer XEVT vm.mockCall( 0x9019Fd383E490B4B045130707C9A1227F36F4636, - abi.encodeWithSelector(Wow.isAllowed.selector), + abi.encodeWithSelector(IXEVT.isAllowed.selector), abi.encode(true) ); @@ -567,7 +569,3 @@ contract MultiBlockHarvestertTest is Fixture, FunctionUtils { harvester.setYieldBearingAssetData(yieldBearingAsset, stablecoin, targetExposure, minExposure, maxExposure, 1); } } - -interface Wow { - function isAllowed(address) external returns (bool); -} From d473b8497313674459f8eea6d736301962634e69 Mon Sep 17 00:00:00 2001 From: 0xtekgrinder <0xtekgrinder@protonmail.com> Date: Tue, 22 Oct 2024 21:35:05 +0200 Subject: [PATCH 44/69] feat: refactor access control so there is only one onlyTrusted mapping --- contracts/helpers/BaseHarvester.sol | 32 ++++++++++++++++------- contracts/helpers/MultiBlockHarvester.sol | 19 -------------- contracts/utils/Errors.sol | 1 + 3 files changed, 23 insertions(+), 29 deletions(-) diff --git a/contracts/helpers/BaseHarvester.sol b/contracts/helpers/BaseHarvester.sol index ab5c314d..3d492ad4 100644 --- a/contracts/helpers/BaseHarvester.sol +++ b/contracts/helpers/BaseHarvester.sol @@ -35,8 +35,20 @@ abstract contract BaseHarvester is IHarvester, AccessControl { MODIFIERS //////////////////////////////////////////////////////////////////////////////////////////////////////////////////*/ - modifier onlyAllowedOrGuardian() { - if (!isAllowed[msg.sender] && !accessControlManager.isGovernorOrGuardian(msg.sender)) revert NotTrusted(); + /** + * @notice Checks whether the `msg.sender` is trusted to update target exposure and do others non critical operations + */ + modifier onlyTrusted() { + if (!isTrusted[msg.sender]) revert NotTrusted(); + _; + } + + /** + * @notice Checks whether the `msg.sender` is trusted or guardian to update target exposure and do others non critical operations + */ + modifier onlyTrustedOrGuardian() { + if (!isTrusted[msg.sender] && !accessControlManager.isGovernorOrGuardian(msg.sender)) + revert NotTrustedOrGuardian(); _; } @@ -52,8 +64,8 @@ abstract contract BaseHarvester is IHarvester, AccessControl { uint96 public maxSlippage; /// @notice Data associated to a yield bearing asset mapping(address => YieldBearingParams) public yieldBearingData; - /// @notice Whether an address is allowed to update the target exposure of a yield bearing asset - mapping(address => bool) public isAllowed; + /// @notice trusted addresses that can update target exposure and do others non critical operations + mapping(address => bool) public isTrusted; /*////////////////////////////////////////////////////////////////////////////////////////////////////////////////// CONSTRUCTOR @@ -121,15 +133,15 @@ abstract contract BaseHarvester is IHarvester, AccessControl { } /** - * @notice add an address to the allowed list - * @param account address to be added + * @notice Toggle the trusted status of an address + * @param trusted address to toggle the trusted status */ - function toggleAllowed(address account) external onlyGuardian { - isAllowed[account] = !isAllowed[account]; + function toggleTrusted(address trusted) external onlyGuardian { + isTrusted[trusted] = !isTrusted[trusted]; } /*////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - ALLOWED FUNCTIONS + TRUSTED FUNCTIONS //////////////////////////////////////////////////////////////////////////////////////////////////////////////////*/ /** @@ -137,7 +149,7 @@ abstract contract BaseHarvester is IHarvester, AccessControl { * @param yieldBearingAsset address of the yield bearing asset * @param targetExposure target exposure to the yield bearing asset used */ - function setTargetExposure(address yieldBearingAsset, uint64 targetExposure) external onlyAllowedOrGuardian { + function setTargetExposure(address yieldBearingAsset, uint64 targetExposure) external onlyTrustedOrGuardian { yieldBearingData[yieldBearingAsset].targetExposure = targetExposure; } diff --git a/contracts/helpers/MultiBlockHarvester.sol b/contracts/helpers/MultiBlockHarvester.sol index c8dd0f4a..f24ced78 100644 --- a/contracts/helpers/MultiBlockHarvester.sol +++ b/contracts/helpers/MultiBlockHarvester.sol @@ -20,23 +20,12 @@ import "../utils/Constants.sol"; contract MultiBlockHarvester is BaseHarvester { using SafeERC20 for IERC20; - /*////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - MODIFIERS - //////////////////////////////////////////////////////////////////////////////////////////////////////////////////*/ - - modifier onlyTrusted() { - if (!isTrusted[msg.sender]) revert NotTrusted(); - _; - } - /*////////////////////////////////////////////////////////////////////////////////////////////////////////////////// VARIABLES //////////////////////////////////////////////////////////////////////////////////////////////////////////////////*/ /// @notice address to deposit to receive yieldBearingAsset mapping(address => address) public yieldBearingToDepositAddress; - /// @notice trusted addresses - mapping(address => bool) public isTrusted; /// @notice Maximum amount of stablecoins that can be minted in a single transaction uint256 public maxMintAmount; @@ -83,14 +72,6 @@ contract MultiBlockHarvester is BaseHarvester { yieldBearingToDepositAddress[yieldBearingAsset] = newDepositAddress; } - /** - * @notice Toggle the trusted status of an address - * @param trusted address to toggle the trusted status - */ - function toggleTrusted(address trusted) external onlyGuardian { - isTrusted[trusted] = !isTrusted[trusted]; - } - /*////////////////////////////////////////////////////////////////////////////////////////////////////////////////// TRUSTED FUNCTIONS //////////////////////////////////////////////////////////////////////////////////////////////////////////////////*/ diff --git a/contracts/utils/Errors.sol b/contracts/utils/Errors.sol index 7f553756..00585f9c 100644 --- a/contracts/utils/Errors.sol +++ b/contracts/utils/Errors.sol @@ -31,6 +31,7 @@ error NotCollateral(); error NotGovernor(); error NotGovernorOrGuardian(); error NotTrusted(); +error NotTrustedOrGuardian(); error NotWhitelisted(); error OneInchSwapFailed(); error OracleUpdateFailed(); From 3bc5b0c780ed7d111968753a2913346bcc73d935 Mon Sep 17 00:00:00 2001 From: 0xtekgrinder <0xtekgrinder@protonmail.com> Date: Tue, 22 Oct 2024 21:42:09 +0200 Subject: [PATCH 45/69] style: fix lint of comments --- contracts/helpers/BaseHarvester.sol | 6 ++++-- contracts/helpers/GenericHarvester.sol | 4 ++-- foundry.toml | 2 +- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/contracts/helpers/BaseHarvester.sol b/contracts/helpers/BaseHarvester.sol index 3d492ad4..31412c64 100644 --- a/contracts/helpers/BaseHarvester.sol +++ b/contracts/helpers/BaseHarvester.sol @@ -36,7 +36,8 @@ abstract contract BaseHarvester is IHarvester, AccessControl { //////////////////////////////////////////////////////////////////////////////////////////////////////////////////*/ /** - * @notice Checks whether the `msg.sender` is trusted to update target exposure and do others non critical operations + * @notice Checks whether the `msg.sender` is trusted to update + * target exposure and do others non critical operations */ modifier onlyTrusted() { if (!isTrusted[msg.sender]) revert NotTrusted(); @@ -44,7 +45,8 @@ abstract contract BaseHarvester is IHarvester, AccessControl { } /** - * @notice Checks whether the `msg.sender` is trusted or guardian to update target exposure and do others non critical operations + * @notice Checks whether the `msg.sender` is trusted or guardian to update + * target exposure and do others non critical operations */ modifier onlyTrustedOrGuardian() { if (!isTrusted[msg.sender] && !accessControlManager.isGovernorOrGuardian(msg.sender)) diff --git a/contracts/helpers/GenericHarvester.sol b/contracts/helpers/GenericHarvester.sol index 834c6292..b013fbb7 100644 --- a/contracts/helpers/GenericHarvester.sol +++ b/contracts/helpers/GenericHarvester.sol @@ -66,7 +66,7 @@ contract GenericHarvester is BaseHarvester, IERC3156FlashBorrower, RouterSwapper } /*////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - BUDGET HANDLING + BUDGET HANDLING //////////////////////////////////////////////////////////////////////////////////////////////////////////////////*/ /** @@ -92,7 +92,7 @@ contract GenericHarvester is BaseHarvester, IERC3156FlashBorrower, RouterSwapper } /*////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - HARVEST + HARVEST //////////////////////////////////////////////////////////////////////////////////////////////////////////////////*/ /// @notice Invests or divests from the yield asset associated to `yieldBearingAsset` based on the current exposure diff --git a/foundry.toml b/foundry.toml index 66e0b4f2..a569f06d 100644 --- a/foundry.toml +++ b/foundry.toml @@ -51,7 +51,7 @@ src = 'contracts' gas_reports = ["*"] [profile.dev.fuzz] -runs = 2000 +runs = 200000 [profile.dev.invariant] runs = 100 From bcda179121328f3c28f348a9b7a734630b728b62 Mon Sep 17 00:00:00 2001 From: 0xtekgrinder <0xtekgrinder@protonmail.com> Date: Tue, 22 Oct 2024 22:02:55 +0200 Subject: [PATCH 46/69] tests: update tests to add missing functionnalities --- test/fuzz/MultiBlockHarvester.t.sol | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/test/fuzz/MultiBlockHarvester.t.sol b/test/fuzz/MultiBlockHarvester.t.sol index 7eed9a38..7cd5c802 100644 --- a/test/fuzz/MultiBlockHarvester.t.sol +++ b/test/fuzz/MultiBlockHarvester.t.sol @@ -211,13 +211,21 @@ contract MultiBlockHarvestertTest is Fixture, FunctionUtils { vm.expectRevert(Errors.NotGovernorOrGuardian.selector); harvester.setMaxSlippage(1e9); - vm.expectRevert(Errors.NotGovernorOrGuardian.selector); - harvester.updateLimitExposuresYieldAsset(address(XEVT)); - vm.expectRevert(Errors.NotGovernorOrGuardian.selector); harvester.toggleTrusted(alice); } + function test_OnlyTrusted_RevertWhen_NotTrusted() public { + vm.expectRevert(Errors.NotTrustedOrGuardian.selector); + harvester.setTargetExposure(address(EURC), targetExposure); + + vm.expectRevert(Errors.NotTrusted.selector); + harvester.harvest(XEVT, 1e9, new bytes(0)); + + vm.expectRevert(Errors.NotTrusted.selector); + harvester.finalizeRebalance(EURC, 1e6); + } + function test_SettersHarvester() public { vm.startPrank(governor); vm.expectRevert(Errors.InvalidParam.selector); @@ -314,6 +322,13 @@ contract MultiBlockHarvestertTest is Fixture, FunctionUtils { vm.stopPrank(); } + function test_SetTargetExposure() public { + vm.prank(governor); + harvester.setTargetExposure(address(EURC), targetExposure + 1); + (, uint64 currentTargetExposure, , , ) = harvester.yieldBearingData(address(EURC)); + assertEq(currentTargetExposure, targetExposure + 1); + } + function test_harvest_TooBigMintedAmount() external { _loadReserve(EURC, 1e26); _loadReserve(XEVT, 1e6); From 2af51dff553048f9cca47164b29163be6f76ac80 Mon Sep 17 00:00:00 2001 From: 0xtekgrinder <0xtekgrinder@protonmail.com> Date: Fri, 25 Oct 2024 10:22:48 +0200 Subject: [PATCH 47/69] feat: remove mint and burn from MultiBlockHarvester --- contracts/helpers/MultiBlockHarvester.sol | 21 +++++++++------------ test/fuzz/MultiBlockHarvester.t.sol | 14 ++++++-------- 2 files changed, 15 insertions(+), 20 deletions(-) diff --git a/contracts/helpers/MultiBlockHarvester.sol b/contracts/helpers/MultiBlockHarvester.sol index f24ced78..6a63b048 100644 --- a/contracts/helpers/MultiBlockHarvester.sol +++ b/contracts/helpers/MultiBlockHarvester.sol @@ -27,21 +27,21 @@ contract MultiBlockHarvester is BaseHarvester { /// @notice address to deposit to receive yieldBearingAsset mapping(address => address) public yieldBearingToDepositAddress; - /// @notice Maximum amount of stablecoins that can be minted in a single transaction - uint256 public maxMintAmount; + /// @notice Maximum amount of stablecoins that can be used in a single transaction + uint256 public maxOrderAmount; /*////////////////////////////////////////////////////////////////////////////////////////////////////////////////// CONSTRUCTOR //////////////////////////////////////////////////////////////////////////////////////////////////////////////////*/ constructor( - uint256 initialMaxMintAmount, + uint256 initialOrderMintAmount, uint96 initialMaxSlippage, IAccessControlManager definitiveAccessControlManager, IAgToken definitiveAgToken, ITransmuter definitiveTransmuter ) BaseHarvester(initialMaxSlippage, definitiveAccessControlManager, definitiveAgToken, definitiveTransmuter) { - maxMintAmount = initialMaxMintAmount; + maxOrderAmount = initialOrderMintAmount; } /*////////////////////////////////////////////////////////////////////////////////////////////////////////////////// @@ -49,11 +49,11 @@ contract MultiBlockHarvester is BaseHarvester { //////////////////////////////////////////////////////////////////////////////////////////////////////////////////*/ /** - * @notice Set the maximum amount of stablecoins that can be minted in a single transaction - * @param newMaxMintAmount new maximum amount of stablecoins that can be minted in a single transaction + * @notice Set the maximum amount of stablecoins that can be used in a single transaction + * @param newMaxOrderAmount new maximum amount of stablecoins that can be used in a single transaction */ - function setMaxMintAmount(uint256 newMaxMintAmount) external onlyGovernor { - maxMintAmount = newMaxMintAmount; + function setMaxOrderAmount(uint256 newMaxOrderAmount) external onlyGovernor { + maxOrderAmount = newMaxOrderAmount; } /*////////////////////////////////////////////////////////////////////////////////////////////////////////////////// @@ -111,7 +111,6 @@ contract MultiBlockHarvester is BaseHarvester { ? yieldBearingToDepositAddress[yieldBearingAsset] : address(0); _checkSlippage(balance, amountOut, yieldBearingAsset, depositAddress, true); - agToken.burnSelf(amountOut, address(this)); } /*////////////////////////////////////////////////////////////////////////////////////////////////////////////////// @@ -124,8 +123,7 @@ contract MultiBlockHarvester is BaseHarvester { YieldBearingParams memory yieldBearingInfo, uint256 amount ) internal { - if (amount > maxMintAmount) revert TooBigAmountIn(); - agToken.mint(address(this), amount); + if (amount > maxOrderAmount) revert TooBigAmountIn(); _adjustAllowance(address(agToken), address(transmuter), amount); if (typeAction == 1) { address depositAddress = yieldBearingToDepositAddress[yieldBearingAsset]; @@ -151,7 +149,6 @@ contract MultiBlockHarvester is BaseHarvester { address(this), block.timestamp ); - agToken.burnSelf(amountOut, address(this)); } else if (yieldBearingAsset == USDM) { uint256 amountOut = transmuter.swapExactInput( amount, diff --git a/test/fuzz/MultiBlockHarvester.t.sol b/test/fuzz/MultiBlockHarvester.t.sol index 7cd5c802..5eb05004 100644 --- a/test/fuzz/MultiBlockHarvester.t.sol +++ b/test/fuzz/MultiBlockHarvester.t.sol @@ -180,6 +180,8 @@ contract MultiBlockHarvestertTest is Fixture, FunctionUtils { transmuter.toggleTrusted(address(harvester), TrustedType.Seller); + agToken.mint(address(harvester), 1_000_000e18); + vm.stopPrank(); vm.label(XEVT, "XEVT"); @@ -191,7 +193,7 @@ contract MultiBlockHarvestertTest is Fixture, FunctionUtils { function test_Initialization() public { assertEq(harvester.maxSlippage(), 1e8); - assertEq(harvester.maxMintAmount(), 100_000e18); + assertEq(harvester.maxOrderAmount(), 100_000e18); assertEq(address(harvester.accessControlManager()), address(accessControlManager)); assertEq(address(harvester.agToken()), address(agToken)); assertEq(address(harvester.transmuter()), address(transmuter)); @@ -234,6 +236,9 @@ contract MultiBlockHarvestertTest is Fixture, FunctionUtils { harvester.setMaxSlippage(123456); assertEq(harvester.maxSlippage(), 123456); + harvester.setMaxOrderAmount(123456); + assertEq(harvester.maxOrderAmount(), 123456); + harvester.setYieldBearingAssetData( address(XEVT), address(EURC), @@ -359,7 +364,6 @@ contract MultiBlockHarvestertTest is Fixture, FunctionUtils { assertEq(IERC20(XEVT).balanceOf(address(harvester)), 0); assertEq(IERC20(EURC).balanceOf(address(harvester)), 0); - assertEq(agToken.balanceOf(address(harvester)), 0); (uint256 issuedFromYieldBearingAsset, uint256 totalIssued) = transmuter.getIssuedByCollateral(XEVT); (uint256 issuedFromStablecoin, ) = transmuter.getIssuedByCollateral(EURC); @@ -391,7 +395,6 @@ contract MultiBlockHarvestertTest is Fixture, FunctionUtils { assertEq(IERC20(EURC).balanceOf(address(harvester)), 0); assertApproxEqRel(IERC20(XEVT).balanceOf(address(harvester)), expectedAmount / 1e12, 1e18); // XEVT is stored in the harvester while the redemption is in progress - assertEq(agToken.balanceOf(address(harvester)), 0); // fake semd EURC to harvester deal(EURC, address(harvester), amount); @@ -399,7 +402,6 @@ contract MultiBlockHarvestertTest is Fixture, FunctionUtils { vm.prank(alice); harvester.finalizeRebalance(EURC, amount); - assertEq(agToken.balanceOf(address(harvester)), 0); assertEq(IERC20(EURC).balanceOf(address(harvester)), 0); (uint256 issuedFromYieldBearingAsset, uint256 totalIssued) = transmuter.getIssuedByCollateral(XEVT); @@ -433,7 +435,6 @@ contract MultiBlockHarvestertTest is Fixture, FunctionUtils { assertEq(IERC20(USDC).balanceOf(address(harvester)), 0); assertEq(IERC20(USDM).balanceOf(address(harvester)), 0); assertApproxEqRel(IERC20(USDM).balanceOf(address(receiver)), expectedAmount, 1e18); - assertEq(agToken.balanceOf(address(harvester)), 0); // fake semd USDC to harvester deal(USDC, address(harvester), amount); @@ -441,7 +442,6 @@ contract MultiBlockHarvestertTest is Fixture, FunctionUtils { vm.prank(alice); harvester.finalizeRebalance(USDC, amount); - assertEq(agToken.balanceOf(address(harvester)), 0); assertEq(IERC20(USDC).balanceOf(address(harvester)), 0); (uint256 issuedFromYieldBearingAsset, uint256 totalIssued) = transmuter.getIssuedByCollateral(USDM); @@ -475,7 +475,6 @@ contract MultiBlockHarvestertTest is Fixture, FunctionUtils { assertEq(IERC20(USDC).balanceOf(address(harvester)), 0); assertEq(IERC20(USDM).balanceOf(address(harvester)), 0); assertApproxEqRel(IERC20(USDM).balanceOf(address(receiver)), expectedAmount, 1e18); - assertEq(agToken.balanceOf(address(harvester)), 0); // fake semd USDC to harvester deal(USDC, address(harvester), amount); @@ -483,7 +482,6 @@ contract MultiBlockHarvestertTest is Fixture, FunctionUtils { vm.prank(alice); harvester.finalizeRebalance(USDC, amount); - assertEq(agToken.balanceOf(address(harvester)), 0); assertEq(IERC20(USDC).balanceOf(address(harvester)), 0); (uint256 issuedFromYieldBearingAsset, uint256 totalIssued) = transmuter.getIssuedByCollateral(USDM); From 29df83349d2aff71263a05cbbb77cafe5d2bc194 Mon Sep 17 00:00:00 2001 From: 0xtekgrinder <0xtekgrinder@protonmail.com> Date: Fri, 25 Oct 2024 10:35:44 +0200 Subject: [PATCH 48/69] doc: typo --- contracts/helpers/MultiBlockHarvester.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/helpers/MultiBlockHarvester.sol b/contracts/helpers/MultiBlockHarvester.sol index 6a63b048..7494f320 100644 --- a/contracts/helpers/MultiBlockHarvester.sol +++ b/contracts/helpers/MultiBlockHarvester.sol @@ -202,7 +202,7 @@ contract MultiBlockHarvester is BaseHarvester { uint256 slippage = ((amountIn - amountOut) * 1e9) / amountIn; if (slippage > maxSlippage) revert SlippageTooHigh(); } else if (asset == XEVT) { - // Assumer 1:1 ratio between the underlying asset of the vault + // Assume 1:1 ratio between the underlying asset of the vault uint256 slippage = ((IPool(depositAddress).convertToAssets(amountIn) - amountOut) * 1e9) / amountIn; if (slippage > maxSlippage) revert SlippageTooHigh(); } else revert InvalidParam(); From 496dda898ca7e083890a58a34f4f268141cf5e0b Mon Sep 17 00:00:00 2001 From: 0xtekgrinder <0xtekgrinder@protonmail.com> Date: Fri, 25 Oct 2024 10:37:51 +0200 Subject: [PATCH 49/69] feat: declare depositAddress in the upper scope --- contracts/helpers/MultiBlockHarvester.sol | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/contracts/helpers/MultiBlockHarvester.sol b/contracts/helpers/MultiBlockHarvester.sol index 7494f320..632e6a0d 100644 --- a/contracts/helpers/MultiBlockHarvester.sol +++ b/contracts/helpers/MultiBlockHarvester.sol @@ -125,9 +125,8 @@ contract MultiBlockHarvester is BaseHarvester { ) internal { if (amount > maxOrderAmount) revert TooBigAmountIn(); _adjustAllowance(address(agToken), address(transmuter), amount); + address depositAddress = yieldBearingToDepositAddress[yieldBearingAsset]; if (typeAction == 1) { - address depositAddress = yieldBearingToDepositAddress[yieldBearingAsset]; - if (yieldBearingAsset == XEVT) { uint256 amountOut = transmuter.swapExactInput( amount, @@ -170,9 +169,7 @@ contract MultiBlockHarvester is BaseHarvester { address(this), block.timestamp ); - address depositAddress = yieldBearingToDepositAddress[yieldBearingAsset]; _checkSlippage(amount, amountOut, yieldBearingAsset, depositAddress, false); - if (yieldBearingAsset == XEVT) { IPool(depositAddress).requestRedeem(amountOut); } else if (yieldBearingAsset == USDM) { From cf6b776168ff5aea6730a53a2c4826e4a0de27b2 Mon Sep 17 00:00:00 2001 From: 0xtekgrinder <0xtekgrinder@protonmail.com> Date: Fri, 25 Oct 2024 10:48:00 +0200 Subject: [PATCH 50/69] feat: simplify rebalance --- contracts/helpers/MultiBlockHarvester.sol | 27 ++++++++--------------- 1 file changed, 9 insertions(+), 18 deletions(-) diff --git a/contracts/helpers/MultiBlockHarvester.sol b/contracts/helpers/MultiBlockHarvester.sol index 632e6a0d..8d84d642 100644 --- a/contracts/helpers/MultiBlockHarvester.sol +++ b/contracts/helpers/MultiBlockHarvester.sol @@ -127,16 +127,16 @@ contract MultiBlockHarvester is BaseHarvester { _adjustAllowance(address(agToken), address(transmuter), amount); address depositAddress = yieldBearingToDepositAddress[yieldBearingAsset]; if (typeAction == 1) { + uint256 amountOut = transmuter.swapExactInput( + amount, + 0, + address(agToken), + yieldBearingInfo.stablecoin, + address(this), + block.timestamp + ); + _checkSlippage(amount, amountOut, yieldBearingInfo.stablecoin, depositAddress, false); if (yieldBearingAsset == XEVT) { - uint256 amountOut = transmuter.swapExactInput( - amount, - 0, - address(agToken), - yieldBearingInfo.stablecoin, - address(this), - block.timestamp - ); - _checkSlippage(amount, amountOut, yieldBearingInfo.stablecoin, depositAddress, false); _adjustAllowance(yieldBearingInfo.stablecoin, address(depositAddress), amountOut); (uint256 shares, ) = IPool(depositAddress).deposit(amountOut, address(this)); _adjustAllowance(yieldBearingAsset, address(transmuter), shares); @@ -149,15 +149,6 @@ contract MultiBlockHarvester is BaseHarvester { block.timestamp ); } else if (yieldBearingAsset == USDM) { - uint256 amountOut = transmuter.swapExactInput( - amount, - 0, - address(agToken), - yieldBearingInfo.stablecoin, - address(this), - block.timestamp - ); - _checkSlippage(amount, amountOut, yieldBearingInfo.stablecoin, depositAddress, false); IERC20(yieldBearingInfo.stablecoin).safeTransfer(depositAddress, amountOut); } } else { From 29b7a5a689c5955f2b37f3176930436f33796985 Mon Sep 17 00:00:00 2001 From: 0xtekgrinder <0xtekgrinder@protonmail.com> Date: Fri, 25 Oct 2024 11:02:33 +0200 Subject: [PATCH 51/69] feat: specify block fork number --- test/fuzz/MultiBlockHarvester.t.sol | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/fuzz/MultiBlockHarvester.t.sol b/test/fuzz/MultiBlockHarvester.t.sol index 5eb05004..54e6133c 100644 --- a/test/fuzz/MultiBlockHarvester.t.sol +++ b/test/fuzz/MultiBlockHarvester.t.sol @@ -42,7 +42,7 @@ contract MultiBlockHarvestertTest is Fixture, FunctionUtils { receiver = makeAddr("receiver"); - vm.createSelectFork("mainnet"); + vm.createSelectFork("mainnet", 21_041_434); // set mint Fees to 0 on all collaterals uint64[] memory xFeeMint = new uint64[](1); @@ -345,7 +345,7 @@ contract MultiBlockHarvestertTest is Fixture, FunctionUtils { } function test_harvest_IncreaseExposureXEVT(uint256 amount) external { - amount = bound(amount, 1e3, 1e11); + amount = 7022; _loadReserve(EURC, amount); _setYieldBearingData(XEVT, EURC); From 2b28e0ffa4a68fc9f90fe807c25e43feb11df4c6 Mon Sep 17 00:00:00 2001 From: 0xtekgrinder <0xtekgrinder@protonmail.com> Date: Fri, 25 Oct 2024 11:26:47 +0200 Subject: [PATCH 52/69] refactor: rename stablecoin to depositAsset --- contracts/helpers/BaseHarvester.sol | 48 +++++++++++------------ contracts/helpers/GenericHarvester.sol | 26 ++++++------ contracts/helpers/MultiBlockHarvester.sol | 8 ++-- 3 files changed, 41 insertions(+), 41 deletions(-) diff --git a/contracts/helpers/BaseHarvester.sol b/contracts/helpers/BaseHarvester.sol index 31412c64..59081fb8 100644 --- a/contracts/helpers/BaseHarvester.sol +++ b/contracts/helpers/BaseHarvester.sol @@ -12,13 +12,13 @@ import "../utils/Errors.sol"; import "../interfaces/IHarvester.sol"; struct YieldBearingParams { - // Address of the stablecoin (ex: USDC) - address stablecoin; + // Address of the asset used to mint the yield bearing asset + address depositAsset; // Target exposure to the collateral yield bearing asset used uint64 targetExposure; - // Maximum exposure within the Transmuter to the stablecoin asset + // Maximum exposure within the Transmuter to the deposit asset uint64 maxExposure; - // Minimum exposure within the Transmuter to the stablecoin asset + // Minimum exposure within the Transmuter to the deposit asset uint64 minExposure; // 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 @@ -92,15 +92,15 @@ abstract contract BaseHarvester is IHarvester, AccessControl { /** * @notice Set the yieldBearingAsset data * @param yieldBearingAsset address of the yieldBearingAsset - * @param stablecoin address of the stablecoin + * @param depositAsset address of the depositAsset * @param targetExposure target exposure to the yieldBearingAsset asset used - * @param minExposure minimum exposure within the Transmuter to the stablecoin - * @param maxExposure maximum exposure within the Transmuter to the stablecoin + * @param minExposure minimum exposure within the Transmuter to the depositAsset + * @param maxExposure maximum exposure within the Transmuter to the depositAsset * @param overrideExposures whether limit exposures should be overriden or read onchain through the Transmuter */ function setYieldBearingAssetData( address yieldBearingAsset, - address stablecoin, + address depositAsset, uint64 targetExposure, uint64 minExposure, uint64 maxExposure, @@ -108,7 +108,7 @@ abstract contract BaseHarvester is IHarvester, AccessControl { ) external onlyGuardian { _setYieldBearingAssetData( yieldBearingAsset, - stablecoin, + depositAsset, targetExposure, minExposure, maxExposure, @@ -117,13 +117,13 @@ abstract contract BaseHarvester is IHarvester, AccessControl { } /** - * @notice Set the limit exposures to the stablecoin linked to the yield bearing asset + * @notice Set the limit exposures to the depositAsset linked to the yield bearing asset * @param yieldBearingAsset address of the yield bearing asset */ function updateLimitExposuresYieldAsset(address yieldBearingAsset) public virtual { YieldBearingParams storage yieldBearingInfo = yieldBearingData[yieldBearingAsset]; if (yieldBearingInfo.overrideExposures == 2) - _updateLimitExposuresYieldAsset(yieldBearingInfo.stablecoin, yieldBearingInfo); + _updateLimitExposuresYieldAsset(yieldBearingInfo.depositAsset, yieldBearingInfo); } /** @@ -180,7 +180,7 @@ abstract contract BaseHarvester is IHarvester, AccessControl { (uint256 stablecoinsFromYieldBearingAsset, uint256 stablecoinsIssued) = transmuter.getIssuedByCollateral( yieldBearingAsset ); - (uint256 stablecoinsFromStablecoin, ) = transmuter.getIssuedByCollateral(yieldBearingInfo.stablecoin); + (uint256 stablecoinsFromDepositAsset, ) = transmuter.getIssuedByCollateral(yieldBearingInfo.depositAsset); uint256 targetExposureScaled = yieldBearingInfo.targetExposure * stablecoinsIssued; if (stablecoinsFromYieldBearingAsset * 1e9 > targetExposureScaled) { // Need to decrease exposure to yield bearing asset @@ -188,31 +188,31 @@ abstract contract BaseHarvester is IHarvester, AccessControl { uint256 maxValueScaled = yieldBearingInfo.maxExposure * 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 (stablecoinsFromStablecoin * 1e9 > maxValueScaled) amount = 0; - else if ((stablecoinsFromStablecoin + amount) * 1e9 > maxValueScaled) - amount = maxValueScaled / 1e9 - stablecoinsFromStablecoin; + if (stablecoinsFromDepositAsset * 1e9 > maxValueScaled) amount = 0; + else if ((stablecoinsFromDepositAsset + amount) * 1e9 > maxValueScaled) + amount = maxValueScaled / 1e9 - stablecoinsFromDepositAsset; } 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 increase = 1; amount = targetExposureScaled / 1e9 - stablecoinsFromYieldBearingAsset; uint256 minValueScaled = yieldBearingInfo.minExposure * stablecoinsIssued; - if (stablecoinsFromStablecoin * 1e9 < minValueScaled) amount = 0; - else if (stablecoinsFromStablecoin * 1e9 < minValueScaled + amount * 1e9) - amount = stablecoinsFromStablecoin - minValueScaled / 1e9; + if (stablecoinsFromDepositAsset * 1e9 < minValueScaled) amount = 0; + else if (stablecoinsFromDepositAsset * 1e9 < minValueScaled + amount * 1e9) + amount = stablecoinsFromDepositAsset - minValueScaled / 1e9; } } function _setYieldBearingAssetData( address yieldBearingAsset, - address stablecoin, + address depositAsset, uint64 targetExposure, uint64 minExposure, uint64 maxExposure, uint64 overrideExposures ) internal virtual { YieldBearingParams storage yieldBearingInfo = yieldBearingData[yieldBearingAsset]; - yieldBearingInfo.stablecoin = stablecoin; + yieldBearingInfo.depositAsset = depositAsset; if (targetExposure >= 1e9) revert InvalidParam(); yieldBearingInfo.targetExposure = targetExposure; yieldBearingInfo.overrideExposures = overrideExposures; @@ -222,22 +222,22 @@ abstract contract BaseHarvester is IHarvester, AccessControl { yieldBearingInfo.minExposure = minExposure; } else { yieldBearingInfo.overrideExposures = 2; - _updateLimitExposuresYieldAsset(yieldBearingInfo.stablecoin, yieldBearingInfo); + _updateLimitExposuresYieldAsset(depositAsset, yieldBearingInfo); } } function _updateLimitExposuresYieldAsset( - address stablecoin, + address depositAsset, YieldBearingParams storage yieldBearingInfo ) internal virtual { uint64[] memory xFeeMint; - (xFeeMint, ) = transmuter.getCollateralMintFees(stablecoin); + (xFeeMint, ) = transmuter.getCollateralMintFees(depositAsset); uint256 length = xFeeMint.length; if (length <= 1) yieldBearingInfo.maxExposure = 1e9; else yieldBearingInfo.maxExposure = xFeeMint[length - 2]; uint64[] memory xFeeBurn; - (xFeeBurn, ) = transmuter.getCollateralBurnFees(stablecoin); + (xFeeBurn, ) = transmuter.getCollateralBurnFees(depositAsset); length = xFeeBurn.length; if (length <= 1) yieldBearingInfo.minExposure = 0; else yieldBearingInfo.minExposure = xFeeBurn[length - 2]; diff --git a/contracts/helpers/GenericHarvester.sol b/contracts/helpers/GenericHarvester.sol index b013fbb7..ca654920 100644 --- a/contracts/helpers/GenericHarvester.sol +++ b/contracts/helpers/GenericHarvester.sol @@ -98,7 +98,7 @@ contract GenericHarvester is BaseHarvester, IERC3156FlashBorrower, RouterSwapper /// @notice Invests or divests from the yield asset associated to `yieldBearingAsset` based on the current exposure /// to this yieldBearingAsset /// @dev This transaction either reduces the exposure to `yieldBearingAsset` in the Transmuter or frees up - /// some yieldBearingAsset that can then be used for people looking to burn stablecoins + /// some yieldBearingAsset that can then be used for people looking to burn deposit tokens /// @dev Due to potential transaction fees within the Transmuter, this function doesn't exactly bring /// `yieldBearingAsset` to the target exposure /// @dev scale is a number between 0 and 1e9 that represents the proportion of the agToken to harvest, @@ -112,13 +112,13 @@ contract GenericHarvester is BaseHarvester, IERC3156FlashBorrower, RouterSwapper (SwapType swapType, bytes memory data) = abi.decode(extraData, (SwapType, bytes)); if (amount > 0) { - try transmuter.updateOracle(yieldBearingInfo.stablecoin) {} catch {} + try transmuter.updateOracle(yieldBearingInfo.depositAsset) {} catch {} adjustYieldExposure( amount, increase, yieldBearingAsset, - yieldBearingInfo.stablecoin, + yieldBearingInfo.depositAsset, (amount * (1e9 - maxSlippage)) / 1e9, swapType, data @@ -126,17 +126,17 @@ contract GenericHarvester is BaseHarvester, IERC3156FlashBorrower, RouterSwapper } } - /// @notice Burns `amountStablecoins` for one yieldBearing asset, swap for asset then mints stablecoins + /// @notice Burns `amountStablecoins` for one yieldBearing asset, swap for asset then mints deposit tokens /// 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 stablecoin, 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 + /// burning agToken for the deposit asset, swapping for the yield bearing asset, then minting the agToken + /// @dev This function reverts if the second agToken mint gives less than `minAmountOut` of ag tokens /// @dev This function reverts if the swap slippage is higher than `maxSlippage` function adjustYieldExposure( uint256 amountStablecoins, uint8 increase, address yieldBearingAsset, - address stablecoin, + address depositAsset, uint256 minAmountOut, SwapType swapType, bytes memory extraData @@ -145,7 +145,7 @@ contract GenericHarvester is BaseHarvester, IERC3156FlashBorrower, RouterSwapper IERC3156FlashBorrower(address(this)), address(agToken), amountStablecoins, - abi.encode(msg.sender, increase, yieldBearingAsset, stablecoin, minAmountOut, swapType, extraData) + abi.encode(msg.sender, increase, yieldBearingAsset, depositAsset, minAmountOut, swapType, extraData) ); } @@ -167,19 +167,19 @@ contract GenericHarvester is BaseHarvester, IERC3156FlashBorrower, RouterSwapper address tokenIn; { address yieldBearingAsset; - address stablecoin; - (sender, typeAction, yieldBearingAsset, stablecoin, minAmountOut, swapType, callData) = abi.decode( + address depositAsset; + (sender, typeAction, yieldBearingAsset, depositAsset, minAmountOut, swapType, callData) = abi.decode( data, (address, uint256, address, address, uint256, SwapType, bytes) ); if (typeAction == 1) { // Increase yield exposure action: we bring in the yield bearing asset tokenOut = yieldBearingAsset; - tokenIn = stablecoin; + tokenIn = depositAsset; } else { - // Decrease yield exposure action: we bring in the liquid stablecoin + // Decrease yield exposure action: we bring in the deposit asset tokenIn = yieldBearingAsset; - tokenOut = stablecoin; + tokenOut = depositAsset; } } uint256 amountOut = transmuter.swapExactInput( diff --git a/contracts/helpers/MultiBlockHarvester.sol b/contracts/helpers/MultiBlockHarvester.sol index 8d84d642..9497c242 100644 --- a/contracts/helpers/MultiBlockHarvester.sol +++ b/contracts/helpers/MultiBlockHarvester.sol @@ -131,13 +131,13 @@ contract MultiBlockHarvester is BaseHarvester { amount, 0, address(agToken), - yieldBearingInfo.stablecoin, + yieldBearingInfo.depositAsset, address(this), block.timestamp ); - _checkSlippage(amount, amountOut, yieldBearingInfo.stablecoin, depositAddress, false); + _checkSlippage(amount, amountOut, yieldBearingInfo.depositAsset, depositAddress, false); if (yieldBearingAsset == XEVT) { - _adjustAllowance(yieldBearingInfo.stablecoin, address(depositAddress), amountOut); + _adjustAllowance(yieldBearingInfo.depositAsset, address(depositAddress), amountOut); (uint256 shares, ) = IPool(depositAddress).deposit(amountOut, address(this)); _adjustAllowance(yieldBearingAsset, address(transmuter), shares); amountOut = transmuter.swapExactInput( @@ -149,7 +149,7 @@ contract MultiBlockHarvester is BaseHarvester { block.timestamp ); } else if (yieldBearingAsset == USDM) { - IERC20(yieldBearingInfo.stablecoin).safeTransfer(depositAddress, amountOut); + IERC20(yieldBearingInfo.depositAsset).safeTransfer(depositAddress, amountOut); } } else { uint256 amountOut = transmuter.swapExactInput( From 791763f6e9564d116f4183b2fbf7392a4d883e6a Mon Sep 17 00:00:00 2001 From: 0xtekgrinder <0xtekgrinder@protonmail.com> Date: Fri, 25 Oct 2024 11:29:28 +0200 Subject: [PATCH 53/69] feat: add unchecked blocks to _checkSlippage --- contracts/helpers/MultiBlockHarvester.sol | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/contracts/helpers/MultiBlockHarvester.sol b/contracts/helpers/MultiBlockHarvester.sol index 9497c242..c26589e8 100644 --- a/contracts/helpers/MultiBlockHarvester.sol +++ b/contracts/helpers/MultiBlockHarvester.sol @@ -187,12 +187,16 @@ contract MultiBlockHarvester is BaseHarvester { if (asset == USDC || asset == USDM || asset == EURC) { // Assume 1:1 ratio between stablecoins - uint256 slippage = ((amountIn - amountOut) * 1e9) / amountIn; - if (slippage > maxSlippage) revert SlippageTooHigh(); + unchecked { + uint256 slippage = ((amountIn - amountOut) * 1e9) / amountIn; + if (slippage > maxSlippage) revert SlippageTooHigh(); + } } else if (asset == XEVT) { // Assume 1:1 ratio between the underlying asset of the vault - uint256 slippage = ((IPool(depositAddress).convertToAssets(amountIn) - amountOut) * 1e9) / amountIn; - if (slippage > maxSlippage) revert SlippageTooHigh(); + unchecked { + uint256 slippage = ((IPool(depositAddress).convertToAssets(amountIn) - amountOut) * 1e9) / amountIn; + if (slippage > maxSlippage) revert SlippageTooHigh(); + } } else revert InvalidParam(); } } From cddfd4bfc626444383894d628cd85f4055942a4d Mon Sep 17 00:00:00 2001 From: 0xtekgrinder <0xtekgrinder@protonmail.com> Date: Fri, 25 Oct 2024 12:57:11 +0200 Subject: [PATCH 54/69] refactor: rename depositAsset to asset --- contracts/helpers/BaseHarvester.sol | 42 +++++++++++------------ contracts/helpers/GenericHarvester.sol | 16 ++++----- contracts/helpers/MultiBlockHarvester.sol | 8 ++--- 3 files changed, 33 insertions(+), 33 deletions(-) diff --git a/contracts/helpers/BaseHarvester.sol b/contracts/helpers/BaseHarvester.sol index 59081fb8..e2029a1f 100644 --- a/contracts/helpers/BaseHarvester.sol +++ b/contracts/helpers/BaseHarvester.sol @@ -13,7 +13,7 @@ import "../interfaces/IHarvester.sol"; struct YieldBearingParams { // Address of the asset used to mint the yield bearing asset - address depositAsset; + address asset; // Target exposure to the collateral yield bearing asset used uint64 targetExposure; // Maximum exposure within the Transmuter to the deposit asset @@ -92,15 +92,15 @@ abstract contract BaseHarvester is IHarvester, AccessControl { /** * @notice Set the yieldBearingAsset data * @param yieldBearingAsset address of the yieldBearingAsset - * @param depositAsset address of the depositAsset + * @param asset address of the asset * @param targetExposure target exposure to the yieldBearingAsset asset used - * @param minExposure minimum exposure within the Transmuter to the depositAsset - * @param maxExposure maximum exposure within the Transmuter to the depositAsset + * @param minExposure minimum exposure within the Transmuter to the asset + * @param maxExposure maximum exposure within the Transmuter to the asset * @param overrideExposures whether limit exposures should be overriden or read onchain through the Transmuter */ function setYieldBearingAssetData( address yieldBearingAsset, - address depositAsset, + address asset, uint64 targetExposure, uint64 minExposure, uint64 maxExposure, @@ -108,7 +108,7 @@ abstract contract BaseHarvester is IHarvester, AccessControl { ) external onlyGuardian { _setYieldBearingAssetData( yieldBearingAsset, - depositAsset, + asset, targetExposure, minExposure, maxExposure, @@ -117,13 +117,13 @@ abstract contract BaseHarvester is IHarvester, AccessControl { } /** - * @notice Set the limit exposures to the depositAsset linked to the yield bearing asset + * @notice Set the limit exposures to the asset linked to the yield bearing asset * @param yieldBearingAsset address of the yield bearing asset */ function updateLimitExposuresYieldAsset(address yieldBearingAsset) public virtual { YieldBearingParams storage yieldBearingInfo = yieldBearingData[yieldBearingAsset]; if (yieldBearingInfo.overrideExposures == 2) - _updateLimitExposuresYieldAsset(yieldBearingInfo.depositAsset, yieldBearingInfo); + _updateLimitExposuresYieldAsset(yieldBearingInfo.asset, yieldBearingInfo); } /** @@ -180,7 +180,7 @@ abstract contract BaseHarvester is IHarvester, AccessControl { (uint256 stablecoinsFromYieldBearingAsset, uint256 stablecoinsIssued) = transmuter.getIssuedByCollateral( yieldBearingAsset ); - (uint256 stablecoinsFromDepositAsset, ) = transmuter.getIssuedByCollateral(yieldBearingInfo.depositAsset); + (uint256 stablecoinsFromAsset, ) = transmuter.getIssuedByCollateral(yieldBearingInfo.asset); uint256 targetExposureScaled = yieldBearingInfo.targetExposure * stablecoinsIssued; if (stablecoinsFromYieldBearingAsset * 1e9 > targetExposureScaled) { // Need to decrease exposure to yield bearing asset @@ -188,31 +188,31 @@ abstract contract BaseHarvester is IHarvester, AccessControl { uint256 maxValueScaled = yieldBearingInfo.maxExposure * 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 (stablecoinsFromDepositAsset * 1e9 > maxValueScaled) amount = 0; - else if ((stablecoinsFromDepositAsset + amount) * 1e9 > maxValueScaled) - amount = maxValueScaled / 1e9 - stablecoinsFromDepositAsset; + 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 increase = 1; amount = targetExposureScaled / 1e9 - stablecoinsFromYieldBearingAsset; uint256 minValueScaled = yieldBearingInfo.minExposure * stablecoinsIssued; - if (stablecoinsFromDepositAsset * 1e9 < minValueScaled) amount = 0; - else if (stablecoinsFromDepositAsset * 1e9 < minValueScaled + amount * 1e9) - amount = stablecoinsFromDepositAsset - minValueScaled / 1e9; + if (stablecoinsFromAsset * 1e9 < minValueScaled) amount = 0; + else if (stablecoinsFromAsset * 1e9 < minValueScaled + amount * 1e9) + amount = stablecoinsFromAsset - minValueScaled / 1e9; } } function _setYieldBearingAssetData( address yieldBearingAsset, - address depositAsset, + address asset, uint64 targetExposure, uint64 minExposure, uint64 maxExposure, uint64 overrideExposures ) internal virtual { YieldBearingParams storage yieldBearingInfo = yieldBearingData[yieldBearingAsset]; - yieldBearingInfo.depositAsset = depositAsset; + yieldBearingInfo.asset = asset; if (targetExposure >= 1e9) revert InvalidParam(); yieldBearingInfo.targetExposure = targetExposure; yieldBearingInfo.overrideExposures = overrideExposures; @@ -222,22 +222,22 @@ abstract contract BaseHarvester is IHarvester, AccessControl { yieldBearingInfo.minExposure = minExposure; } else { yieldBearingInfo.overrideExposures = 2; - _updateLimitExposuresYieldAsset(depositAsset, yieldBearingInfo); + _updateLimitExposuresYieldAsset(asset, yieldBearingInfo); } } function _updateLimitExposuresYieldAsset( - address depositAsset, + address asset, YieldBearingParams storage yieldBearingInfo ) internal virtual { uint64[] memory xFeeMint; - (xFeeMint, ) = transmuter.getCollateralMintFees(depositAsset); + (xFeeMint, ) = transmuter.getCollateralMintFees(asset); uint256 length = xFeeMint.length; if (length <= 1) yieldBearingInfo.maxExposure = 1e9; else yieldBearingInfo.maxExposure = xFeeMint[length - 2]; uint64[] memory xFeeBurn; - (xFeeBurn, ) = transmuter.getCollateralBurnFees(depositAsset); + (xFeeBurn, ) = transmuter.getCollateralBurnFees(asset); length = xFeeBurn.length; if (length <= 1) yieldBearingInfo.minExposure = 0; else yieldBearingInfo.minExposure = xFeeBurn[length - 2]; diff --git a/contracts/helpers/GenericHarvester.sol b/contracts/helpers/GenericHarvester.sol index ca654920..0b3bbdc3 100644 --- a/contracts/helpers/GenericHarvester.sol +++ b/contracts/helpers/GenericHarvester.sol @@ -112,13 +112,13 @@ contract GenericHarvester is BaseHarvester, IERC3156FlashBorrower, RouterSwapper (SwapType swapType, bytes memory data) = abi.decode(extraData, (SwapType, bytes)); if (amount > 0) { - try transmuter.updateOracle(yieldBearingInfo.depositAsset) {} catch {} + try transmuter.updateOracle(yieldBearingInfo.asset) {} catch {} adjustYieldExposure( amount, increase, yieldBearingAsset, - yieldBearingInfo.depositAsset, + yieldBearingInfo.asset, (amount * (1e9 - maxSlippage)) / 1e9, swapType, data @@ -136,7 +136,7 @@ contract GenericHarvester is BaseHarvester, IERC3156FlashBorrower, RouterSwapper uint256 amountStablecoins, uint8 increase, address yieldBearingAsset, - address depositAsset, + address asset, uint256 minAmountOut, SwapType swapType, bytes memory extraData @@ -145,7 +145,7 @@ contract GenericHarvester is BaseHarvester, IERC3156FlashBorrower, RouterSwapper IERC3156FlashBorrower(address(this)), address(agToken), amountStablecoins, - abi.encode(msg.sender, increase, yieldBearingAsset, depositAsset, minAmountOut, swapType, extraData) + abi.encode(msg.sender, increase, yieldBearingAsset, asset, minAmountOut, swapType, extraData) ); } @@ -167,19 +167,19 @@ contract GenericHarvester is BaseHarvester, IERC3156FlashBorrower, RouterSwapper address tokenIn; { address yieldBearingAsset; - address depositAsset; - (sender, typeAction, yieldBearingAsset, depositAsset, minAmountOut, swapType, callData) = abi.decode( + address asset; + (sender, typeAction, yieldBearingAsset, asset, minAmountOut, swapType, callData) = abi.decode( data, (address, uint256, address, address, uint256, SwapType, bytes) ); if (typeAction == 1) { // Increase yield exposure action: we bring in the yield bearing asset tokenOut = yieldBearingAsset; - tokenIn = depositAsset; + tokenIn = asset; } else { // Decrease yield exposure action: we bring in the deposit asset tokenIn = yieldBearingAsset; - tokenOut = depositAsset; + tokenOut = asset; } } uint256 amountOut = transmuter.swapExactInput( diff --git a/contracts/helpers/MultiBlockHarvester.sol b/contracts/helpers/MultiBlockHarvester.sol index c26589e8..22b800a9 100644 --- a/contracts/helpers/MultiBlockHarvester.sol +++ b/contracts/helpers/MultiBlockHarvester.sol @@ -131,13 +131,13 @@ contract MultiBlockHarvester is BaseHarvester { amount, 0, address(agToken), - yieldBearingInfo.depositAsset, + yieldBearingInfo.asset, address(this), block.timestamp ); - _checkSlippage(amount, amountOut, yieldBearingInfo.depositAsset, depositAddress, false); + _checkSlippage(amount, amountOut, yieldBearingInfo.asset, depositAddress, false); if (yieldBearingAsset == XEVT) { - _adjustAllowance(yieldBearingInfo.depositAsset, address(depositAddress), amountOut); + _adjustAllowance(yieldBearingInfo.asset, address(depositAddress), amountOut); (uint256 shares, ) = IPool(depositAddress).deposit(amountOut, address(this)); _adjustAllowance(yieldBearingAsset, address(transmuter), shares); amountOut = transmuter.swapExactInput( @@ -149,7 +149,7 @@ contract MultiBlockHarvester is BaseHarvester { block.timestamp ); } else if (yieldBearingAsset == USDM) { - IERC20(yieldBearingInfo.depositAsset).safeTransfer(depositAddress, amountOut); + IERC20(yieldBearingInfo.asset).safeTransfer(depositAddress, amountOut); } } else { uint256 amountOut = transmuter.swapExactInput( From 7a39fc9f21c849255217aa34af1b185a111d6a42 Mon Sep 17 00:00:00 2001 From: 0xtekgrinder <0xtekgrinder@protonmail.com> Date: Fri, 25 Oct 2024 13:00:34 +0200 Subject: [PATCH 55/69] feat: remove MaxOrderAmount check --- contracts/helpers/MultiBlockHarvester.sol | 21 +-------------------- scripts/DeployGenericHarvester.s.sol | 5 ++--- scripts/DeployMultiBlockHarvester.s.sol | 2 -- test/fuzz/MultiBlockHarvester.t.sol | 6 +----- 4 files changed, 4 insertions(+), 30 deletions(-) diff --git a/contracts/helpers/MultiBlockHarvester.sol b/contracts/helpers/MultiBlockHarvester.sol index 22b800a9..fe663364 100644 --- a/contracts/helpers/MultiBlockHarvester.sol +++ b/contracts/helpers/MultiBlockHarvester.sol @@ -27,34 +27,16 @@ contract MultiBlockHarvester is BaseHarvester { /// @notice address to deposit to receive yieldBearingAsset mapping(address => address) public yieldBearingToDepositAddress; - /// @notice Maximum amount of stablecoins that can be used in a single transaction - uint256 public maxOrderAmount; - /*////////////////////////////////////////////////////////////////////////////////////////////////////////////////// CONSTRUCTOR //////////////////////////////////////////////////////////////////////////////////////////////////////////////////*/ constructor( - uint256 initialOrderMintAmount, uint96 initialMaxSlippage, IAccessControlManager definitiveAccessControlManager, IAgToken definitiveAgToken, ITransmuter definitiveTransmuter - ) BaseHarvester(initialMaxSlippage, definitiveAccessControlManager, definitiveAgToken, definitiveTransmuter) { - maxOrderAmount = initialOrderMintAmount; - } - - /*////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - GOVERNOR FUNCTIONS - //////////////////////////////////////////////////////////////////////////////////////////////////////////////////*/ - - /** - * @notice Set the maximum amount of stablecoins that can be used in a single transaction - * @param newMaxOrderAmount new maximum amount of stablecoins that can be used in a single transaction - */ - function setMaxOrderAmount(uint256 newMaxOrderAmount) external onlyGovernor { - maxOrderAmount = newMaxOrderAmount; - } + ) BaseHarvester(initialMaxSlippage, definitiveAccessControlManager, definitiveAgToken, definitiveTransmuter) {} /*////////////////////////////////////////////////////////////////////////////////////////////////////////////////// GUARDIAN FUNCTIONS @@ -123,7 +105,6 @@ contract MultiBlockHarvester is BaseHarvester { YieldBearingParams memory yieldBearingInfo, uint256 amount ) internal { - if (amount > maxOrderAmount) revert TooBigAmountIn(); _adjustAllowance(address(agToken), address(transmuter), amount); address depositAddress = yieldBearingToDepositAddress[yieldBearingAsset]; if (typeAction == 1) { diff --git a/scripts/DeployGenericHarvester.s.sol b/scripts/DeployGenericHarvester.s.sol index a2561559..32229bce 100644 --- a/scripts/DeployGenericHarvester.s.sol +++ b/scripts/DeployGenericHarvester.s.sol @@ -17,12 +17,11 @@ contract DeployGenericHarvester is Utils { address deployer = vm.addr(deployerPrivateKey); console.log("Deployer address: ", deployer); - uint256 maxMintAmount = 1000000e18; uint96 maxSlippage = 1e9 / 100; uint32 maxSwapSlippage = 100; // 1% IERC3156FlashLender flashloan = IERC3156FlashLender(_chainToContract(CHAIN_SOURCE, ContractType.FlashLoan)); - IAgToken agToken = IAgToken(_chainToContract(CHAIN_SOURCE, ContractType.AgEUR)); - ITransmuter transmuter = ITransmuter(_chainToContract(CHAIN_SOURCE, ContractType.TransmuterAgEUR)); + IAgToken agToken = IAgToken(_chainToContract(CHAIN_SOURCE, ContractType.AgUSD)); + ITransmuter transmuter = ITransmuter(_chainToContract(CHAIN_SOURCE, ContractType.TransmuterAgUSD)); IAccessControlManager accessControlManager = transmuter.accessControlManager(); GenericHarvester harvester = new GenericHarvester( diff --git a/scripts/DeployMultiBlockHarvester.s.sol b/scripts/DeployMultiBlockHarvester.s.sol index a38a8c6f..854f82f9 100644 --- a/scripts/DeployMultiBlockHarvester.s.sol +++ b/scripts/DeployMultiBlockHarvester.s.sol @@ -16,14 +16,12 @@ contract DeployMultiBlockHarvester is Utils { address deployer = vm.addr(deployerPrivateKey); console.log("Deployer address: ", deployer); - uint256 maxMintAmount = 1000000e18; uint96 maxSlippage = 1e9 / 100; address agToken = _chainToContract(CHAIN_SOURCE, ContractType.AgEUR); address transmuter = _chainToContract(CHAIN_SOURCE, ContractType.TransmuterAgEUR); IAccessControlManager accessControlManager = ITransmuter(transmuter).accessControlManager(); MultiBlockHarvester harvester = new MultiBlockHarvester( - maxMintAmount, maxSlippage, accessControlManager, IAgToken(agToken), diff --git a/test/fuzz/MultiBlockHarvester.t.sol b/test/fuzz/MultiBlockHarvester.t.sol index 54e6133c..941501e2 100644 --- a/test/fuzz/MultiBlockHarvester.t.sol +++ b/test/fuzz/MultiBlockHarvester.t.sol @@ -172,7 +172,7 @@ contract MultiBlockHarvestertTest is Fixture, FunctionUtils { maxExposureYieldAsset = uint64((90 * 1e9) / 100); minExposureYieldAsset = uint64((5 * 1e9) / 100); - harvester = new MultiBlockHarvester(100_000e18, 1e8, accessControlManager, agToken, transmuter); + harvester = new MultiBlockHarvester(1e8, accessControlManager, agToken, transmuter); vm.startPrank(governor); harvester.toggleTrusted(alice); harvester.setYieldBearingToDepositAddress(XEVT, XEVT); @@ -193,7 +193,6 @@ contract MultiBlockHarvestertTest is Fixture, FunctionUtils { function test_Initialization() public { assertEq(harvester.maxSlippage(), 1e8); - assertEq(harvester.maxOrderAmount(), 100_000e18); assertEq(address(harvester.accessControlManager()), address(accessControlManager)); assertEq(address(harvester.agToken()), address(agToken)); assertEq(address(harvester.transmuter()), address(transmuter)); @@ -236,9 +235,6 @@ contract MultiBlockHarvestertTest is Fixture, FunctionUtils { harvester.setMaxSlippage(123456); assertEq(harvester.maxSlippage(), 123456); - harvester.setMaxOrderAmount(123456); - assertEq(harvester.maxOrderAmount(), 123456); - harvester.setYieldBearingAssetData( address(XEVT), address(EURC), From bfeac9ee9d95f3d98963c9dad4f1deb0962c63a6 Mon Sep 17 00:00:00 2001 From: 0xtekgrinder <0xtekgrinder@protonmail.com> Date: Fri, 25 Oct 2024 13:54:16 +0200 Subject: [PATCH 56/69] tests: remove tooBigAmountIn --- test/fuzz/MultiBlockHarvester.t.sol | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/test/fuzz/MultiBlockHarvester.t.sol b/test/fuzz/MultiBlockHarvester.t.sol index 941501e2..30f66625 100644 --- a/test/fuzz/MultiBlockHarvester.t.sol +++ b/test/fuzz/MultiBlockHarvester.t.sol @@ -330,16 +330,6 @@ contract MultiBlockHarvestertTest is Fixture, FunctionUtils { assertEq(currentTargetExposure, targetExposure + 1); } - function test_harvest_TooBigMintedAmount() external { - _loadReserve(EURC, 1e26); - _loadReserve(XEVT, 1e6); - _setYieldBearingData(XEVT, EURC); - - vm.expectRevert(TooBigAmountIn.selector); - vm.prank(alice); - harvester.harvest(XEVT, 1e9, new bytes(0)); - } - function test_harvest_IncreaseExposureXEVT(uint256 amount) external { amount = 7022; _loadReserve(EURC, amount); From 814602634a8c5e868ce8bd4ef07702b3b0623a96 Mon Sep 17 00:00:00 2001 From: 0xtekgrinder <0xtekgrinder@protonmail.com> Date: Fri, 25 Oct 2024 18:13:32 +0200 Subject: [PATCH 57/69] tests: setup of generic harvester + budget --- contracts/utils/Constants.sol | 3 + test/fuzz/GenericHarvester.t.sol | 160 +++++++++++++++++++++++++++++++ 2 files changed, 163 insertions(+) diff --git a/contracts/utils/Constants.sol b/contracts/utils/Constants.sol index f992da83..f288ffb9 100644 --- a/contracts/utils/Constants.sol +++ b/contracts/utils/Constants.sol @@ -64,5 +64,8 @@ IStETH constant STETH = IStETH(0xae7ab96520DE3A18E5e111B5EaAb095312D7fE84); ISfrxETH constant SFRXETH = ISfrxETH(0xac3E018457B222d93114458476f3E3416Abbe38F); address constant XEVT = 0x3Ee320c9F73a84D1717557af00695A34b26d1F1d; address constant USDM = 0x59D9356E565Ab3A36dD77763Fc0d87fEaf85508C; +address constant STEAK_USDC = 0xBEEF01735c132Ada46AA9aA4c54623cAA92A64CB; address constant USDC = 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48; address constant EURC = 0x1aBaEA1f7C830bD89Acc67eC4af516284b1bC33c; + +address constant ONEINCH_ROUTER = 0x111111125421cA6dc452d289314280a0f8842A65; diff --git a/test/fuzz/GenericHarvester.t.sol b/test/fuzz/GenericHarvester.t.sol index e69de29b..f3b718d3 100644 --- a/test/fuzz/GenericHarvester.t.sol +++ b/test/fuzz/GenericHarvester.t.sol @@ -0,0 +1,160 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.19; + +import { SafeERC20 } from "oz/token/ERC20/utils/SafeERC20.sol"; + +import "forge-std/Test.sol"; + +import "contracts/utils/Errors.sol" as Errors; + +import { IERC20Metadata } from "../mock/MockTokenPermit.sol"; +import "../utils/FunctionUtils.sol"; + +import "contracts/savings/Savings.sol"; +import "../mock/MockTokenPermit.sol"; +import "contracts/helpers/GenericHarvester.sol"; + +import "contracts/transmuter/Storage.sol"; +import "contracts/utils/Constants.sol"; + +import { IAccessControl } from "oz/access/IAccessControl.sol"; + +import { ContractType, CommonUtils, CHAIN_ETHEREUM } from "utils/src/CommonUtils.sol"; + +contract GenericHarvestertTest is Test, FunctionUtils, CommonUtils { + using SafeERC20 for IERC20; + + GenericHarvester public harvester; + uint64 public targetExposure; + uint64 public maxExposureYieldAsset; + uint64 public minExposureYieldAsset; + address governor; + IAgToken agToken; + IERC3156FlashLender flashloan; + ITransmuter transmuter; + IAccessControlManager accessControlManager; + + address alice = vm.addr(1); + + function setUp() public { + uint256 CHAIN_SOURCE = CHAIN_ETHEREUM; + + vm.createSelectFork("mainnet", 21_041_434); + + targetExposure = uint64((15 * 1e9) / 100); + maxExposureYieldAsset = uint64((90 * 1e9) / 100); + minExposureYieldAsset = uint64((5 * 1e9) / 100); + + flashloan = IERC3156FlashLender(_chainToContract(CHAIN_SOURCE, ContractType.FlashLoan)); + transmuter = ITransmuter(_chainToContract(CHAIN_SOURCE, ContractType.TransmuterAgUSD)); + agToken = IAgToken(_chainToContract(CHAIN_SOURCE, ContractType.AgUSD)); + accessControlManager = transmuter.accessControlManager(); + governor = _chainToContract(CHAIN_SOURCE, ContractType.GovernorMultisig); + + harvester = new GenericHarvester( + 1e8, + ONEINCH_ROUTER, + ONEINCH_ROUTER, + 100, + agToken, + transmuter, + accessControlManager, + flashloan + ); + vm.startPrank(governor); + harvester.toggleTrusted(alice); + + transmuter.toggleTrusted(address(harvester), TrustedType.Seller); + vm.stopPrank(); + + vm.label(STEAK_USDC, "STEAK_USDC"); + vm.label(USDC, "USDC"); + vm.label(address(harvester), "Harvester"); + } + + function test_Initialization() public { + assertEq(harvester.maxSlippage(), 1e8); + assertEq(address(harvester.accessControlManager()), address(accessControlManager)); + assertEq(address(harvester.agToken()), address(agToken)); + assertEq(address(harvester.transmuter()), address(transmuter)); + assertEq(address(harvester.flashloan()), address(flashloan)); + assertEq(harvester.maxSwapSlippage(), 100); + assertEq(harvester.tokenTransferAddress(), ONEINCH_ROUTER); + assertEq(harvester.swapRouter(), ONEINCH_ROUTER); + } + + function test_AddBudget(uint256 amount, address receiver) public { + vm.assume(receiver != address(0)); + amount = bound(amount, 1e18, 1e21); + + deal(address(agToken), alice, amount); + vm.startPrank(alice); + agToken.approve(address(harvester), type(uint256).max); + harvester.addBudget(amount, receiver); + vm.stopPrank(); + + assertEq(harvester.budget(receiver), amount); + assertEq(agToken.balanceOf(address(harvester)), amount); + } + + function test_RemoveBudget(uint256 amount) public { + amount = bound(amount, 1e18, 1e21); + + deal(address(agToken), alice, amount); + vm.startPrank(alice); + agToken.approve(address(harvester), type(uint256).max); + harvester.addBudget(amount, alice); + vm.stopPrank(); + + assertEq(harvester.budget(alice), amount); + assertEq(agToken.balanceOf(address(harvester)), amount); + + vm.startPrank(alice); + harvester.removeBudget(amount, alice); + vm.stopPrank(); + + assertEq(harvester.budget(alice), 0); + assertEq(agToken.balanceOf(address(harvester)), 0); + assertEq(agToken.balanceOf(alice), amount); + } + + function test_IncreaseExposure_STEAK_USDC() public {} + + function test_DecreaseExposure_STEAK_USDC() public {} + + function _loadReserve(address token, uint256 amount) internal { + if (token == USDM) { + vm.prank(0x48AEB395FB0E4ff8433e9f2fa6E0579838d33B62); + IAgToken(USDM).mint(alice, amount); + } else { + deal(token, alice, amount); + } + + vm.startPrank(alice); + IERC20(token).approve(address(transmuter), type(uint256).max); + transmuter.swapExactInput(amount, 0, token, address(agToken), alice, block.timestamp + 1); + vm.stopPrank(); + } + + function _setYieldBearingData(address yieldBearingAsset, address stablecoin) internal { + vm.prank(governor); + harvester.setYieldBearingAssetData( + yieldBearingAsset, + stablecoin, + targetExposure, + minExposureYieldAsset, + maxExposureYieldAsset, + 1 + ); + } + + function _setYieldBearingData( + address yieldBearingAsset, + address stablecoin, + uint64 minExposure, + uint64 maxExposure + ) internal { + vm.prank(governor); + harvester.setYieldBearingAssetData(yieldBearingAsset, stablecoin, targetExposure, minExposure, maxExposure, 1); + } +} From 08d8a28421521e4f10b836d36e1f30a7a815e748 Mon Sep 17 00:00:00 2001 From: 0xtekgrinder <0xtekgrinder@protonmail.com> Date: Fri, 25 Oct 2024 18:19:17 +0200 Subject: [PATCH 58/69] tests: setters --- test/fuzz/GenericHarvester.t.sol | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/test/fuzz/GenericHarvester.t.sol b/test/fuzz/GenericHarvester.t.sol index f3b718d3..0260a817 100644 --- a/test/fuzz/GenericHarvester.t.sol +++ b/test/fuzz/GenericHarvester.t.sol @@ -83,6 +83,32 @@ contract GenericHarvestertTest is Test, FunctionUtils, CommonUtils { assertEq(harvester.swapRouter(), ONEINCH_ROUTER); } + function test_Setters() public { + vm.expectRevert(Errors.NotGovernorOrGuardian.selector); + harvester.setTokenTransferAddress(alice); + + vm.startPrank(governor); + harvester.setTokenTransferAddress(alice); + assertEq(harvester.tokenTransferAddress(), alice); + vm.stopPrank(); + + vm.expectRevert(Errors.NotGovernorOrGuardian.selector); + harvester.setSwapRouter(alice); + + vm.startPrank(governor); + harvester.setSwapRouter(alice); + assertEq(harvester.swapRouter(), alice); + vm.stopPrank(); + + vm.expectRevert(Errors.NotGovernorOrGuardian.selector); + harvester.setMaxSlippage(1e9); + + vm.startPrank(governor); + harvester.setMaxSlippage(1e9); + assertEq(harvester.maxSlippage(), 1e9); + vm.stopPrank(); + } + function test_AddBudget(uint256 amount, address receiver) public { vm.assume(receiver != address(0)); amount = bound(amount, 1e18, 1e21); From e7fd84f204f869e6bb3ac6798678792ac3651bdf Mon Sep 17 00:00:00 2001 From: 0xtekgrinder <0xtekgrinder@protonmail.com> Date: Fri, 25 Oct 2024 18:34:53 +0200 Subject: [PATCH 59/69] feat: revert if zero amount for GenericHarvester --- contracts/helpers/GenericHarvester.sol | 25 +++++++++++-------------- test/fuzz/GenericHarvester.t.sol | 9 +++++++-- 2 files changed, 18 insertions(+), 16 deletions(-) diff --git a/contracts/helpers/GenericHarvester.sol b/contracts/helpers/GenericHarvester.sol index 0b3bbdc3..fe2754fe 100644 --- a/contracts/helpers/GenericHarvester.sol +++ b/contracts/helpers/GenericHarvester.sol @@ -108,22 +108,19 @@ contract GenericHarvester is BaseHarvester, IERC3156FlashBorrower, RouterSwapper YieldBearingParams memory yieldBearingInfo = yieldBearingData[yieldBearingAsset]; (uint8 increase, uint256 amount) = _computeRebalanceAmount(yieldBearingAsset, yieldBearingInfo); amount = (amount * scale) / 1e9; + if (amount == 0) revert ZeroAmount(); (SwapType swapType, bytes memory data) = abi.decode(extraData, (SwapType, bytes)); - - if (amount > 0) { - try transmuter.updateOracle(yieldBearingInfo.asset) {} catch {} - - adjustYieldExposure( - amount, - increase, - yieldBearingAsset, - yieldBearingInfo.asset, - (amount * (1e9 - maxSlippage)) / 1e9, - swapType, - data - ); - } + try transmuter.updateOracle(yieldBearingInfo.asset) {} catch {} + adjustYieldExposure( + amount, + increase, + yieldBearingAsset, + yieldBearingInfo.asset, + (amount * (1e9 - maxSlippage)) / 1e9, + swapType, + data + ); } /// @notice Burns `amountStablecoins` for one yieldBearing asset, swap for asset then mints deposit tokens diff --git a/test/fuzz/GenericHarvester.t.sol b/test/fuzz/GenericHarvester.t.sol index 0260a817..72249e58 100644 --- a/test/fuzz/GenericHarvester.t.sol +++ b/test/fuzz/GenericHarvester.t.sol @@ -144,9 +144,14 @@ contract GenericHarvestertTest is Test, FunctionUtils, CommonUtils { assertEq(agToken.balanceOf(alice), amount); } - function test_IncreaseExposure_STEAK_USDC() public {} + function test_Harvest_ZeroAmount() public { + vm.expectRevert(Errors.ZeroAmount.selector); + harvester.harvest(STEAK_USDC, 1e9, abi.encode(uint8(SwapType.VAULT), new bytes(0))); + } + + function test_Harvest_IncreaseExposureSTEAK_USDC() public {} - function test_DecreaseExposure_STEAK_USDC() public {} + function test_Harvest_DecreaseExposureSTEAK_USDC() public {} function _loadReserve(address token, uint256 amount) internal { if (token == USDM) { From 2d0c3d2aa99ca71ee11b76d8a4e8e1b52e76b94c Mon Sep 17 00:00:00 2001 From: 0xtekgrinder <0xtekgrinder@protonmail.com> Date: Mon, 28 Oct 2024 12:55:19 +0100 Subject: [PATCH 60/69] fix: correct generic harvester swaps --- contracts/helpers/GenericHarvester.sol | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/contracts/helpers/GenericHarvester.sol b/contracts/helpers/GenericHarvester.sol index fe2754fe..bd4af92b 100644 --- a/contracts/helpers/GenericHarvester.sol +++ b/contracts/helpers/GenericHarvester.sol @@ -183,19 +183,19 @@ contract GenericHarvester is BaseHarvester, IERC3156FlashBorrower, RouterSwapper amount, 0, address(agToken), - tokenOut, + tokenIn, address(this), block.timestamp ); // Swap to tokenIn - amountOut = _swapToTokenOut(typeAction, tokenOut, tokenIn, amountOut, swapType, callData); + amountOut = _swapToTokenOut(typeAction, tokenIn, tokenOut, amountOut, swapType, callData); - _adjustAllowance(tokenIn, address(transmuter), amountOut); + _adjustAllowance(tokenOut, address(transmuter), amountOut); uint256 amountStableOut = transmuter.swapExactInput( amountOut, minAmountOut, - tokenIn, + tokenOut, address(agToken), address(this), block.timestamp @@ -256,6 +256,7 @@ contract GenericHarvester is BaseHarvester, IERC3156FlashBorrower, RouterSwapper uint256 decimalsTokenIn = IERC20Metadata(tokenIn).decimals(); uint256 decimalsTokenOut = IERC20Metadata(tokenOut).decimals(); amount = _scaleAmountBasedOnDecimals(decimalsTokenIn, decimalsTokenOut, amount, true); + // TODO fix slippage if (amountOut < (amount * (BPS - maxSwapSlippage)) / BPS) { revert SlippageTooHigh(); } @@ -302,8 +303,8 @@ contract GenericHarvester is BaseHarvester, IERC3156FlashBorrower, RouterSwapper ) internal returns (uint256 amountOut) { if (typeAction == 1) { // Granting allowance with the yieldBearingAsset 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)); + _adjustAllowance(tokenIn, tokenOut, amount); + amountOut = IERC4626(tokenOut).deposit(amount, address(this)); + } else amountOut = IERC4626(tokenIn).redeem(amount, address(this), address(this)); } } From 1139a9b1da26e457bac8473570baf93345a1468c Mon Sep 17 00:00:00 2001 From: 0xtekgrinder <0xtekgrinder@protonmail.com> Date: Mon, 28 Oct 2024 12:57:26 +0100 Subject: [PATCH 61/69] tests: add not enough budget test --- test/fuzz/GenericHarvester.t.sol | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/test/fuzz/GenericHarvester.t.sol b/test/fuzz/GenericHarvester.t.sol index 72249e58..bbb8827f 100644 --- a/test/fuzz/GenericHarvester.t.sol +++ b/test/fuzz/GenericHarvester.t.sol @@ -149,10 +149,17 @@ contract GenericHarvestertTest is Test, FunctionUtils, CommonUtils { harvester.harvest(STEAK_USDC, 1e9, abi.encode(uint8(SwapType.VAULT), new bytes(0))); } - function test_Harvest_IncreaseExposureSTEAK_USDC() public {} + function test_Harvest_NotEnoughBudget() public { + _setYieldBearingData(STEAK_USDC, USDC); + + vm.expectRevert(stdError.arithmeticError); + harvester.harvest(STEAK_USDC, 1e3, abi.encode(uint8(SwapType.VAULT), new bytes(0))); + } function test_Harvest_DecreaseExposureSTEAK_USDC() public {} + function test_Harvest_IncreaseExposureSTEAK_USDC() public {} + function _loadReserve(address token, uint256 amount) internal { if (token == USDM) { vm.prank(0x48AEB395FB0E4ff8433e9f2fa6E0579838d33B62); From 094f958b2ead5a1cb08366438217ab55e76fadb7 Mon Sep 17 00:00:00 2001 From: 0xtekgrinder <0xtekgrinder@protonmail.com> Date: Mon, 28 Oct 2024 13:57:10 +0100 Subject: [PATCH 62/69] feat: remove swap slippage --- contracts/helpers/GenericHarvester.sol | 19 ------------------- scripts/DeployGenericHarvester.s.sol | 4 +--- 2 files changed, 1 insertion(+), 22 deletions(-) diff --git a/contracts/helpers/GenericHarvester.sol b/contracts/helpers/GenericHarvester.sol index bd4af92b..8ffa5bf9 100644 --- a/contracts/helpers/GenericHarvester.sol +++ b/contracts/helpers/GenericHarvester.sol @@ -49,7 +49,6 @@ contract GenericHarvester is BaseHarvester, IERC3156FlashBorrower, RouterSwapper uint96 initialMaxSlippage, address initialTokenTransferAddress, address initialSwapRouter, - uint32 initialMaxSwapSlippage, IAgToken definitiveAgToken, ITransmuter definitiveTransmuter, IAccessControlManager definitiveAccessControlManager, @@ -62,7 +61,6 @@ contract GenericHarvester is BaseHarvester, IERC3156FlashBorrower, RouterSwapper flashloan = definitiveFlashloan; IERC20(agToken).safeApprove(address(definitiveFlashloan), type(uint256).max); - maxSwapSlippage = initialMaxSwapSlippage; } /*////////////////////////////////////////////////////////////////////////////////////////////////////////////////// @@ -226,14 +224,6 @@ contract GenericHarvester is BaseHarvester, IERC3156FlashBorrower, RouterSwapper super.setSwapRouter(newSwapRouter); } - /** - * @notice Set the max swap slippage - * @param newMaxSwapSlippage max slippage in BPS - */ - function setMaxSwapSlippage(uint32 newMaxSwapSlippage) external onlyGuardian { - maxSwapSlippage = newMaxSwapSlippage; - } - /*////////////////////////////////////////////////////////////////////////////////////////////////////////////////// INTERNALS //////////////////////////////////////////////////////////////////////////////////////////////////////////////////*/ @@ -251,15 +241,6 @@ contract GenericHarvester is BaseHarvester, IERC3156FlashBorrower, RouterSwapper } else if (swapType == SwapType.VAULT) { amountOut = _swapToTokenOutVault(typeAction, tokenIn, tokenOut, amount); } - - // Check for slippage - uint256 decimalsTokenIn = IERC20Metadata(tokenIn).decimals(); - uint256 decimalsTokenOut = IERC20Metadata(tokenOut).decimals(); - amount = _scaleAmountBasedOnDecimals(decimalsTokenIn, decimalsTokenOut, amount, true); - // TODO fix slippage - if (amountOut < (amount * (BPS - maxSwapSlippage)) / BPS) { - revert SlippageTooHigh(); - } } /** diff --git a/scripts/DeployGenericHarvester.s.sol b/scripts/DeployGenericHarvester.s.sol index 32229bce..2849bb8c 100644 --- a/scripts/DeployGenericHarvester.s.sol +++ b/scripts/DeployGenericHarvester.s.sol @@ -17,8 +17,7 @@ contract DeployGenericHarvester is Utils { address deployer = vm.addr(deployerPrivateKey); console.log("Deployer address: ", deployer); - uint96 maxSlippage = 1e9 / 100; - uint32 maxSwapSlippage = 100; // 1% + uint96 maxSlippage = 1e9 / 100; // 1% IERC3156FlashLender flashloan = IERC3156FlashLender(_chainToContract(CHAIN_SOURCE, ContractType.FlashLoan)); IAgToken agToken = IAgToken(_chainToContract(CHAIN_SOURCE, ContractType.AgUSD)); ITransmuter transmuter = ITransmuter(_chainToContract(CHAIN_SOURCE, ContractType.TransmuterAgUSD)); @@ -28,7 +27,6 @@ contract DeployGenericHarvester is Utils { maxSlippage, ONEINCH_ROUTER, ONEINCH_ROUTER, - maxSwapSlippage, agToken, transmuter, accessControlManager, From bf111b9fb707c4cf7ebde788f7f1faeaf82830c2 Mon Sep 17 00:00:00 2001 From: 0xtekgrinder <0xtekgrinder@protonmail.com> Date: Mon, 28 Oct 2024 13:59:47 +0100 Subject: [PATCH 63/69] tests: increase and decrease steak_USDC exposure --- test/fuzz/GenericHarvester.t.sol | 77 +++++++++++++++++++++++--------- 1 file changed, 57 insertions(+), 20 deletions(-) diff --git a/test/fuzz/GenericHarvester.t.sol b/test/fuzz/GenericHarvester.t.sol index bbb8827f..3d0f4c5e 100644 --- a/test/fuzz/GenericHarvester.t.sol +++ b/test/fuzz/GenericHarvester.t.sol @@ -55,7 +55,6 @@ contract GenericHarvestertTest is Test, FunctionUtils, CommonUtils { 1e8, ONEINCH_ROUTER, ONEINCH_ROUTER, - 100, agToken, transmuter, accessControlManager, @@ -156,21 +155,57 @@ contract GenericHarvestertTest is Test, FunctionUtils, CommonUtils { harvester.harvest(STEAK_USDC, 1e3, abi.encode(uint8(SwapType.VAULT), new bytes(0))); } - function test_Harvest_DecreaseExposureSTEAK_USDC() public {} + function test_Harvest_DecreaseExposureSTEAK_USDC() public { + _setYieldBearingData(STEAK_USDC, USDC); + _addBudget(1e30, alice); - function test_Harvest_IncreaseExposureSTEAK_USDC() public {} + uint256 beforeBudget = harvester.budget(alice); - function _loadReserve(address token, uint256 amount) internal { - if (token == USDM) { - vm.prank(0x48AEB395FB0E4ff8433e9f2fa6E0579838d33B62); - IAgToken(USDM).mint(alice, amount); - } else { - deal(token, alice, amount); - } + (uint8 expectedIncrease, uint256 expectedAmount) = harvester.computeRebalanceAmount(STEAK_USDC); + assertEq(expectedIncrease, 0); - vm.startPrank(alice); - IERC20(token).approve(address(transmuter), type(uint256).max); - transmuter.swapExactInput(amount, 0, token, address(agToken), alice, block.timestamp + 1); + vm.prank(alice); + harvester.harvest(STEAK_USDC, 1e3, abi.encode(uint8(SwapType.VAULT), new bytes(0))); + + assertEq(IERC20(STEAK_USDC).balanceOf(address(harvester)), 0); + assertEq(IERC20(USDC).balanceOf(address(harvester)), 0); + + (uint8 increase, uint256 amount) = harvester.computeRebalanceAmount(STEAK_USDC); + assertEq(increase, 0); // There is still a small amount to mint because of the transmuter fees and slippage + assertLt(amount, expectedAmount); + + uint256 afterBudget = harvester.budget(alice); + assertLt(afterBudget, beforeBudget); + } + + function test_Harvest_IncreaseExposureSTEAK_USDC() public { + _setYieldBearingData(STEAK_USDC, USDC, uint64((50 * 1e9) / 100)); + _addBudget(1e30, alice); + + uint256 beforeBudget = harvester.budget(alice); + + (uint8 expectedIncrease, uint256 expectedAmount) = harvester.computeRebalanceAmount(STEAK_USDC); + assertEq(expectedIncrease, 1); + + vm.prank(alice); + harvester.harvest(STEAK_USDC, 1e9, abi.encode(uint8(SwapType.VAULT), new bytes(0))); + + assertEq(IERC20(STEAK_USDC).balanceOf(address(harvester)), 0); + assertEq(IERC20(USDC).balanceOf(address(harvester)), 0); + + (uint8 increase, uint256 amount) = harvester.computeRebalanceAmount(STEAK_USDC); + assertEq(increase, 1); // There is still a small amount to mint because of the transmuter fees and slippage + assertLt(amount, expectedAmount); + + uint256 afterBudget = harvester.budget(alice); + assertLt(afterBudget, beforeBudget); + } + + function _addBudget(uint256 amount, address owner) internal { + deal(address(agToken), owner, amount); + vm.startPrank(owner); + agToken.approve(address(harvester), amount); + harvester.addBudget(amount, owner); vm.stopPrank(); } @@ -186,13 +221,15 @@ contract GenericHarvestertTest is Test, FunctionUtils, CommonUtils { ); } - function _setYieldBearingData( - address yieldBearingAsset, - address stablecoin, - uint64 minExposure, - uint64 maxExposure - ) internal { + function _setYieldBearingData(address yieldBearingAsset, address stablecoin, uint64 newTargetExposure) internal { vm.prank(governor); - harvester.setYieldBearingAssetData(yieldBearingAsset, stablecoin, targetExposure, minExposure, maxExposure, 1); + harvester.setYieldBearingAssetData( + yieldBearingAsset, + stablecoin, + newTargetExposure, + minExposureYieldAsset, + maxExposureYieldAsset, + 1 + ); } } From e303ae03f56a5333181aa3f9957df71ca8d622e3 Mon Sep 17 00:00:00 2001 From: 0xtekgrinder <0xtekgrinder@protonmail.com> Date: Mon, 28 Oct 2024 14:13:32 +0100 Subject: [PATCH 64/69] feat: remove swapSlippage variable --- contracts/helpers/GenericHarvester.sol | 2 -- test/fuzz/GenericHarvester.t.sol | 1 - 2 files changed, 3 deletions(-) diff --git a/contracts/helpers/GenericHarvester.sol b/contracts/helpers/GenericHarvester.sol index 8ffa5bf9..f3e9d07a 100644 --- a/contracts/helpers/GenericHarvester.sol +++ b/contracts/helpers/GenericHarvester.sol @@ -36,8 +36,6 @@ contract GenericHarvester is BaseHarvester, IERC3156FlashBorrower, RouterSwapper /// @notice Angle stablecoin flashloan contract IERC3156FlashLender public immutable flashloan; - /// @notice Maximum slippage for swaps - uint32 public maxSwapSlippage; /// @notice Budget of AGToken available for each users mapping(address => uint256) public budget; diff --git a/test/fuzz/GenericHarvester.t.sol b/test/fuzz/GenericHarvester.t.sol index 3d0f4c5e..8ce99453 100644 --- a/test/fuzz/GenericHarvester.t.sol +++ b/test/fuzz/GenericHarvester.t.sol @@ -77,7 +77,6 @@ contract GenericHarvestertTest is Test, FunctionUtils, CommonUtils { assertEq(address(harvester.agToken()), address(agToken)); assertEq(address(harvester.transmuter()), address(transmuter)); assertEq(address(harvester.flashloan()), address(flashloan)); - assertEq(harvester.maxSwapSlippage(), 100); assertEq(harvester.tokenTransferAddress(), ONEINCH_ROUTER); assertEq(harvester.swapRouter(), ONEINCH_ROUTER); } From 0fc36d0482c205d8e7760806427ed54894b711a8 Mon Sep 17 00:00:00 2001 From: 0xtekgrinder <0xtekgrinder@protonmail.com> Date: Tue, 29 Oct 2024 11:02:49 +0100 Subject: [PATCH 65/69] feat: set maxSlippage to 0.3% --- scripts/DeployGenericHarvester.s.sol | 2 +- scripts/DeployMultiBlockHarvester.s.sol | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/DeployGenericHarvester.s.sol b/scripts/DeployGenericHarvester.s.sol index 2849bb8c..6c72c068 100644 --- a/scripts/DeployGenericHarvester.s.sol +++ b/scripts/DeployGenericHarvester.s.sol @@ -17,7 +17,7 @@ contract DeployGenericHarvester is Utils { address deployer = vm.addr(deployerPrivateKey); console.log("Deployer address: ", deployer); - uint96 maxSlippage = 1e9 / 100; // 1% + uint96 maxSlippage = 0.3e7; // 0.3% IERC3156FlashLender flashloan = IERC3156FlashLender(_chainToContract(CHAIN_SOURCE, ContractType.FlashLoan)); IAgToken agToken = IAgToken(_chainToContract(CHAIN_SOURCE, ContractType.AgUSD)); ITransmuter transmuter = ITransmuter(_chainToContract(CHAIN_SOURCE, ContractType.TransmuterAgUSD)); diff --git a/scripts/DeployMultiBlockHarvester.s.sol b/scripts/DeployMultiBlockHarvester.s.sol index 854f82f9..da93d300 100644 --- a/scripts/DeployMultiBlockHarvester.s.sol +++ b/scripts/DeployMultiBlockHarvester.s.sol @@ -16,7 +16,7 @@ contract DeployMultiBlockHarvester is Utils { address deployer = vm.addr(deployerPrivateKey); console.log("Deployer address: ", deployer); - uint96 maxSlippage = 1e9 / 100; + uint96 maxSlippage = 0.3e7; // 0.3% address agToken = _chainToContract(CHAIN_SOURCE, ContractType.AgEUR); address transmuter = _chainToContract(CHAIN_SOURCE, ContractType.TransmuterAgEUR); IAccessControlManager accessControlManager = ITransmuter(transmuter).accessControlManager(); From c85bfb89585f1a55292aaad775b8da227c70db61 Mon Sep 17 00:00:00 2001 From: 0xtekgrinder <0xtekgrinder@protonmail.com> Date: Tue, 29 Oct 2024 11:27:00 +0100 Subject: [PATCH 66/69] tests: add generic harvester swap test --- test/fuzz/GenericHarvester.t.sol | 39 ++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/test/fuzz/GenericHarvester.t.sol b/test/fuzz/GenericHarvester.t.sol index 8ce99453..07de8b41 100644 --- a/test/fuzz/GenericHarvester.t.sol +++ b/test/fuzz/GenericHarvester.t.sol @@ -17,6 +17,8 @@ import "contracts/helpers/GenericHarvester.sol"; import "contracts/transmuter/Storage.sol"; import "contracts/utils/Constants.sol"; +import "../mock/MockRouter.sol"; + import { IAccessControl } from "oz/access/IAccessControl.sol"; import { ContractType, CommonUtils, CHAIN_ETHEREUM } from "utils/src/CommonUtils.sol"; @@ -154,6 +156,43 @@ contract GenericHarvestertTest is Test, FunctionUtils, CommonUtils { harvester.harvest(STEAK_USDC, 1e3, abi.encode(uint8(SwapType.VAULT), new bytes(0))); } + function test_Harvest_Aggregator() public { + _addBudget(1e30, alice); + _setYieldBearingData(STEAK_USDC, USDC); + + uint256 beforeBudget = harvester.budget(alice); + + (uint8 expectedIncrease, uint256 expectedAmount) = harvester.computeRebalanceAmount(STEAK_USDC); + assertEq(expectedIncrease, 0); + + MockRouter router = new MockRouter(); + vm.startPrank(governor); + harvester.setTokenTransferAddress(address(router)); + harvester.setSwapRouter(address(router)); + vm.stopPrank(); + deal(USDC, address(router), 1e6); + + vm.prank(alice); + harvester.adjustYieldExposure( + 1e18, + 0, + STEAK_USDC, + USDC, + 0, + SwapType.SWAP, + abi.encodeWithSelector(MockRouter.swap.selector, 9e17, STEAK_USDC, 1e6, USDC) + ); + + assertEq(IERC20(USDC).balanceOf(address(harvester)), 0); + + (uint8 increase, uint256 amount) = harvester.computeRebalanceAmount(STEAK_USDC); + assertEq(increase, 0); // There is still a small amount to mint because of the transmuter fees and slippage + assertLt(amount, expectedAmount); + + uint256 afterBudget = harvester.budget(alice); + assertEq(afterBudget, beforeBudget); // positive slippage + } + function test_Harvest_DecreaseExposureSTEAK_USDC() public { _setYieldBearingData(STEAK_USDC, USDC); _addBudget(1e30, alice); From b05e00cb517f43e427fc0cd870eb54aeb37de476 Mon Sep 17 00:00:00 2001 From: 0xtekgrinder <0xtekgrinder@protonmail.com> Date: Tue, 29 Oct 2024 11:41:22 +0100 Subject: [PATCH 67/69] tests: add decimals tests --- test/fuzz/MultiBlockHarvester.t.sol | 17 +++++++++++++++++ test/mock/MockScaleDecimals.sol | 24 ++++++++++++++++++++++++ 2 files changed, 41 insertions(+) create mode 100644 test/mock/MockScaleDecimals.sol diff --git a/test/fuzz/MultiBlockHarvester.t.sol b/test/fuzz/MultiBlockHarvester.t.sol index 30f66625..f728b5ed 100644 --- a/test/fuzz/MultiBlockHarvester.t.sol +++ b/test/fuzz/MultiBlockHarvester.t.sol @@ -13,6 +13,7 @@ import "../utils/FunctionUtils.sol"; import "contracts/savings/Savings.sol"; import "../mock/MockTokenPermit.sol"; +import "../mock/MockScaleDecimals.sol"; import "contracts/helpers/MultiBlockHarvester.sol"; import "contracts/transmuter/Storage.sol"; @@ -330,6 +331,22 @@ contract MultiBlockHarvestertTest is Fixture, FunctionUtils { assertEq(currentTargetExposure, targetExposure + 1); } + function test_ScaleDecimals() public { + MockScaleDecimals decimals = new MockScaleDecimals(1e8, accessControlManager, agToken, transmuter); + + uint256 amount = decimals.scaleDecimals(18, 20, 1e18, true); + assertEq(amount, 1e20); + + amount = decimals.scaleDecimals(20, 18, 1e18, false); + assertEq(amount, 1e20); + + amount = decimals.scaleDecimals(18, 20, 1e18, false); + assertEq(amount, 1e16); + + amount = decimals.scaleDecimals(20, 18, 1e18, true); + assertEq(amount, 1e16); + } + function test_harvest_IncreaseExposureXEVT(uint256 amount) external { amount = 7022; _loadReserve(EURC, amount); diff --git a/test/mock/MockScaleDecimals.sol b/test/mock/MockScaleDecimals.sol new file mode 100644 index 00000000..e82a70a8 --- /dev/null +++ b/test/mock/MockScaleDecimals.sol @@ -0,0 +1,24 @@ +// SPDX-License-Identifier: Unlicensed +pragma solidity ^0.8.19; + +import "contracts/helpers/BaseHarvester.sol"; + +contract MockScaleDecimals is BaseHarvester { + constructor( + uint96 initialMaxSlippage, + IAccessControlManager definitiveAccessControlManager, + IAgToken definitiveAgToken, + ITransmuter definitiveTransmuter + ) BaseHarvester(initialMaxSlippage, definitiveAccessControlManager, definitiveAgToken, definitiveTransmuter) {} + + function harvest(address yieldBearingAsset, uint256 scale, bytes calldata extraData) external override {} + + function scaleDecimals( + uint256 decimalsTokenIn, + uint256 decimalsTokenOut, + uint256 amountIn, + bool assetIn + ) external pure returns (uint256) { + return _scaleAmountBasedOnDecimals(decimalsTokenIn, decimalsTokenOut, amountIn, assetIn); + } +} From 671eb769e63e3993b621c62c027a734870b1be70 Mon Sep 17 00:00:00 2001 From: 0xtekgrinder <0xtekgrinder@protonmail.com> Date: Tue, 29 Oct 2024 17:49:05 +0100 Subject: [PATCH 68/69] feat: recover ERC20s --- contracts/helpers/BaseHarvester.sol | 10 ++++++++++ test/fuzz/MultiBlockHarvester.t.sol | 11 +++++++++-- 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/contracts/helpers/BaseHarvester.sol b/contracts/helpers/BaseHarvester.sol index e2029a1f..d83b512b 100644 --- a/contracts/helpers/BaseHarvester.sol +++ b/contracts/helpers/BaseHarvester.sol @@ -142,6 +142,16 @@ abstract contract BaseHarvester is IHarvester, AccessControl { isTrusted[trusted] = !isTrusted[trusted]; } + /** + * @notice Recover ERC20 tokens + * @param tokenAddress address of the token to recover + * @param amountToRecover amount to recover + * @param to address to send the recovered tokens + */ + function recoverERC20(address tokenAddress, uint256 amountToRecover, address to) external onlyGuardian { + IERC20(tokenAddress).safeTransfer(to, amountToRecover); + } + /*////////////////////////////////////////////////////////////////////////////////////////////////////////////////// TRUSTED FUNCTIONS //////////////////////////////////////////////////////////////////////////////////////////////////////////////////*/ diff --git a/test/fuzz/MultiBlockHarvester.t.sol b/test/fuzz/MultiBlockHarvester.t.sol index f728b5ed..2bdbd510 100644 --- a/test/fuzz/MultiBlockHarvester.t.sol +++ b/test/fuzz/MultiBlockHarvester.t.sol @@ -179,8 +179,6 @@ contract MultiBlockHarvestertTest is Fixture, FunctionUtils { harvester.setYieldBearingToDepositAddress(XEVT, XEVT); harvester.setYieldBearingToDepositAddress(USDM, receiver); - transmuter.toggleTrusted(address(harvester), TrustedType.Seller); - agToken.mint(address(harvester), 1_000_000e18); vm.stopPrank(); @@ -457,6 +455,15 @@ contract MultiBlockHarvestertTest is Fixture, FunctionUtils { assertEq(increase, 1); // There is still a small amount to mint because of the transmuter fees and slippage } + function test_harvest_RecoverERC20() external { + deal(USDC, address(harvester), 1e12); + vm.startPrank(governor); + harvester.recoverERC20(USDC, 1e12, alice); + + assertEq(IERC20(USDC).balanceOf(address(harvester)), 0); + assertEq(IERC20(USDC).balanceOf(alice), 1e12); + } + function test_harvest_DecreaseExposureUSDM(uint256 amount) external { amount = bound(amount, 1e15, 1e23); _loadReserve(USDM, amount); From e5d7e51443da61e0c9142a6d0919da8bd1e1a4f6 Mon Sep 17 00:00:00 2001 From: 0xtekgrinder <0xtekgrinder@protonmail.com> Date: Tue, 29 Oct 2024 18:00:24 +0100 Subject: [PATCH 69/69] feat: events for harvester --- contracts/helpers/BaseHarvester.sol | 9 +++++++++ test/fuzz/GenericHarvester.t.sol | 12 ++++++++++++ test/fuzz/MultiBlockHarvester.t.sol | 21 ++++++++++++--------- 3 files changed, 33 insertions(+), 9 deletions(-) diff --git a/contracts/helpers/BaseHarvester.sol b/contracts/helpers/BaseHarvester.sol index d83b512b..97cd9c93 100644 --- a/contracts/helpers/BaseHarvester.sol +++ b/contracts/helpers/BaseHarvester.sol @@ -69,6 +69,13 @@ abstract contract BaseHarvester is IHarvester, AccessControl { /// @notice trusted addresses that can update target exposure and do others non critical operations mapping(address => bool) public isTrusted; + /*////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + EVENTS + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////*/ + + event Recovered(address token, uint256 amount, address to); + event TrustedToggled(address trusted, bool status); + /*////////////////////////////////////////////////////////////////////////////////////////////////////////////////// CONSTRUCTOR //////////////////////////////////////////////////////////////////////////////////////////////////////////////////*/ @@ -139,6 +146,7 @@ abstract contract BaseHarvester is IHarvester, AccessControl { * @param trusted address to toggle the trusted status */ function toggleTrusted(address trusted) external onlyGuardian { + emit TrustedToggled(trusted, isTrusted[trusted]); isTrusted[trusted] = !isTrusted[trusted]; } @@ -149,6 +157,7 @@ abstract contract BaseHarvester is IHarvester, AccessControl { * @param to address to send the recovered tokens */ function recoverERC20(address tokenAddress, uint256 amountToRecover, address to) external onlyGuardian { + emit Recovered(tokenAddress, amountToRecover, to); IERC20(tokenAddress).safeTransfer(to, amountToRecover); } diff --git a/test/fuzz/GenericHarvester.t.sol b/test/fuzz/GenericHarvester.t.sol index 07de8b41..bb505e58 100644 --- a/test/fuzz/GenericHarvester.t.sol +++ b/test/fuzz/GenericHarvester.t.sol @@ -107,6 +107,9 @@ contract GenericHarvestertTest is Test, FunctionUtils, CommonUtils { harvester.setMaxSlippage(1e9); assertEq(harvester.maxSlippage(), 1e9); vm.stopPrank(); + + vm.expectRevert(Errors.NotGovernorOrGuardian.selector); + harvester.recoverERC20(USDC, 1e12, alice); } function test_AddBudget(uint256 amount, address receiver) public { @@ -144,6 +147,15 @@ contract GenericHarvestertTest is Test, FunctionUtils, CommonUtils { assertEq(agToken.balanceOf(alice), amount); } + function test_harvest_RecoverERC20() external { + deal(USDC, address(harvester), 1e12); + vm.startPrank(governor); + harvester.recoverERC20(USDC, 1e12, alice); + + assertEq(IERC20(USDC).balanceOf(address(harvester)), 0); + assertEq(IERC20(USDC).balanceOf(alice), 1e12); + } + function test_Harvest_ZeroAmount() public { vm.expectRevert(Errors.ZeroAmount.selector); harvester.harvest(STEAK_USDC, 1e9, abi.encode(uint8(SwapType.VAULT), new bytes(0))); diff --git a/test/fuzz/MultiBlockHarvester.t.sol b/test/fuzz/MultiBlockHarvester.t.sol index 2bdbd510..113441af 100644 --- a/test/fuzz/MultiBlockHarvester.t.sol +++ b/test/fuzz/MultiBlockHarvester.t.sol @@ -213,6 +213,9 @@ contract MultiBlockHarvestertTest is Fixture, FunctionUtils { vm.expectRevert(Errors.NotGovernorOrGuardian.selector); harvester.toggleTrusted(alice); + + vm.expectRevert(Errors.NotGovernorOrGuardian.selector); + harvester.recoverERC20(USDC, 1e12, alice); } function test_OnlyTrusted_RevertWhen_NotTrusted() public { @@ -322,6 +325,15 @@ contract MultiBlockHarvestertTest is Fixture, FunctionUtils { vm.stopPrank(); } + function test_harvest_RecoverERC20() external { + deal(USDC, address(harvester), 1e12); + vm.startPrank(governor); + harvester.recoverERC20(USDC, 1e12, alice); + + assertEq(IERC20(USDC).balanceOf(address(harvester)), 0); + assertEq(IERC20(USDC).balanceOf(alice), 1e12); + } + function test_SetTargetExposure() public { vm.prank(governor); harvester.setTargetExposure(address(EURC), targetExposure + 1); @@ -455,15 +467,6 @@ contract MultiBlockHarvestertTest is Fixture, FunctionUtils { assertEq(increase, 1); // There is still a small amount to mint because of the transmuter fees and slippage } - function test_harvest_RecoverERC20() external { - deal(USDC, address(harvester), 1e12); - vm.startPrank(governor); - harvester.recoverERC20(USDC, 1e12, alice); - - assertEq(IERC20(USDC).balanceOf(address(harvester)), 0); - assertEq(IERC20(USDC).balanceOf(alice), 1e12); - } - function test_harvest_DecreaseExposureUSDM(uint256 amount) external { amount = bound(amount, 1e15, 1e23); _loadReserve(USDM, amount);