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/BaseHarvester.sol b/contracts/helpers/BaseHarvester.sol index 4196c82f..97cd9c93 100644 --- a/contracts/helpers/BaseHarvester.sol +++ b/contracts/helpers/BaseHarvester.sol @@ -2,25 +2,24 @@ pragma solidity ^0.8.23; -import { SafeCast } from "oz/utils/math/SafeCast.sol"; - -import { ITransmuter } from "interfaces/ITransmuter.sol"; - +import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import { AccessControl, IAccessControlManager } from "../utils/AccessControl.sol"; -import "../utils/Constants.sol"; -import "../utils/Errors.sol"; +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import { ITransmuter } from "../interfaces/ITransmuter.sol"; +import { IAgToken } from "../interfaces/IAgToken.sol"; -import { IRebalancerFlashloan } from "../interfaces/IRebalancerFlashloan.sol"; +import "../utils/Errors.sol"; +import "../interfaces/IHarvester.sol"; -struct CollatParams { - // Yield bearing asset associated to the collateral +struct YieldBearingParams { + // Address of the asset used to mint the yield bearing asset address asset; - // Target exposure to the collateral asset used + // Target exposure to the collateral yield bearing asset used uint64 targetExposure; - // Maximum exposure within the Transmuter to the asset - uint64 maxExposureYieldAsset; - // Minimum exposure within the Transmuter to the asset - uint64 minExposureYieldAsset; + // Maximum exposure within the Transmuter to the deposit asset + uint64 maxExposure; + // 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 uint64 overrideExposures; @@ -28,71 +27,184 @@ struct CollatParams { /// @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; +/// @dev Abstract contract for a harvester that aims at rebalancing a Transmuter +abstract contract BaseHarvester is IHarvester, AccessControl { + using SafeERC20 for IERC20; + + /*////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + MODIFIERS + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////*/ + + /** + * @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(); + _; + } + + /*////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + VARIABLES + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////*/ /// @notice Reference to the `transmuter` implementation this contract aims at rebalancing - ITransmuter public immutable TRANSMUTER; - /// @notice Permissioned rebalancer contract - IRebalancerFlashloan public rebalancer; + 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; + /// @notice Data associated to a yield bearing asset + mapping(address => YieldBearingParams) public yieldBearingData; + /// @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); /*////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - INITIALIZATION + CONSTRUCTOR //////////////////////////////////////////////////////////////////////////////////////////////////////////////////*/ constructor( - address _rebalancer, - address collateral, + uint96 initialMaxSlippage, + IAccessControlManager definitiveAccessControlManager, + IAgToken definitiveAgToken, + ITransmuter definitiveTransmuter + ) { + _setMaxSlippage(initialMaxSlippage); + accessControlManager = definitiveAccessControlManager; + agToken = definitiveAgToken; + transmuter = definitiveTransmuter; + } + + /*////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + GUARDIAN FUNCTIONS + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////*/ + + /** + * @notice Set the yieldBearingAsset data + * @param yieldBearingAsset address of the yieldBearingAsset + * @param asset address of the asset + * @param targetExposure target exposure to the yieldBearingAsset asset used + * @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 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, + uint64 minExposure, + uint64 maxExposure, + uint64 overrideExposures + ) external onlyGuardian { + _setYieldBearingAssetData( + yieldBearingAsset, asset, targetExposure, - minExposureYieldAsset, - maxExposureYieldAsset, + minExposure, + maxExposure, overrideExposures ); - _setMaxSlippage(_maxSlippage); + } + + /** + * @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.asset, yieldBearingInfo); + } + + /** + * @notice Set the max allowed slippage + * @param newMaxSlippage new max allowed slippage + */ + function setMaxSlippage(uint96 newMaxSlippage) external onlyGuardian { + _setMaxSlippage(newMaxSlippage); + } + + /** + * @notice Toggle the trusted status of an address + * @param trusted address to toggle the trusted status + */ + function toggleTrusted(address trusted) external onlyGuardian { + emit TrustedToggled(trusted, isTrusted[trusted]); + 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 { + emit Recovered(tokenAddress, amountToRecover, to); + IERC20(tokenAddress).safeTransfer(to, amountToRecover); } /*////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - HARVEST + TRUSTED FUNCTIONS //////////////////////////////////////////////////////////////////////////////////////////////////////////////////*/ - /// @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; + /** + * @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 onlyTrustedOrGuardian { + yieldBearingData[yieldBearingAsset].targetExposure = targetExposure; + } + + /*////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + 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 + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////*/ + + function _computeRebalanceAmount( + address yieldBearingAsset, + YieldBearingParams memory yieldBearingInfo + ) internal view returns (uint8 increase, uint256 amount) { + (uint256 stablecoinsFromYieldBearingAsset, uint256 stablecoinsIssued) = transmuter.getIssuedByCollateral( + yieldBearingAsset + ); + (uint256 stablecoinsFromAsset, ) = transmuter.getIssuedByCollateral(yieldBearingInfo.asset); + uint256 targetExposureScaled = yieldBearingInfo.targetExposure * stablecoinsIssued; + if (stablecoinsFromYieldBearingAsset * 1e9 > targetExposureScaled) { + // Need to decrease exposure to yield bearing asset + amount = stablecoinsFromYieldBearingAsset - targetExposureScaled / 1e9; + 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 (stablecoinsFromAsset * 1e9 > maxValueScaled) amount = 0; @@ -101,102 +213,88 @@ contract BaseHarvester is AccessControl { } else { // In this case, exposure after the operation might remain slightly below the targetExposure as less // collateral may be obtained by burning stablecoins for the yield asset and unwrapping it - amount = targetExposureScaled / 1e9 - stablecoinsFromCollateral; - uint256 minValueScaled = collatInfo.minExposureYieldAsset * stablecoinsIssued; + increase = 1; + amount = targetExposureScaled / 1e9 - stablecoinsFromYieldBearingAsset; + uint256 minValueScaled = yieldBearingInfo.minExposure * 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, + function _setYieldBearingAssetData( + address yieldBearingAsset, address asset, uint64 targetExposure, - uint64 minExposureYieldAsset, - uint64 maxExposureYieldAsset, + uint64 minExposure, + uint64 maxExposure, uint64 overrideExposures ) internal virtual { - CollatParams storage collatInfo = collateralData[collateral]; - collatInfo.asset = asset; + YieldBearingParams storage yieldBearingInfo = yieldBearingData[yieldBearingAsset]; + yieldBearingInfo.asset = asset; 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; + if (maxExposure >= 1e9 || minExposure >= maxExposure) revert InvalidParam(); + yieldBearingInfo.maxExposure = maxExposure; + yieldBearingInfo.minExposure = minExposure; } else { - collatInfo.overrideExposures = 2; - _updateLimitExposuresYieldAsset(collatInfo); + yieldBearingInfo.overrideExposures = 2; + _updateLimitExposuresYieldAsset(asset, yieldBearingInfo); } } - function _updateLimitExposuresYieldAsset(CollatParams storage collatInfo) internal virtual { + function _updateLimitExposuresYieldAsset( + address asset, + YieldBearingParams storage yieldBearingInfo + ) internal virtual { uint64[] memory xFeeMint; - (xFeeMint, ) = TRANSMUTER.getCollateralMintFees(collatInfo.asset); + (xFeeMint, ) = transmuter.getCollateralMintFees(asset); uint256 length = xFeeMint.length; - if (length <= 1) collatInfo.maxExposureYieldAsset = 1e9; - else collatInfo.maxExposureYieldAsset = xFeeMint[length - 2]; + if (length <= 1) yieldBearingInfo.maxExposure = 1e9; + else yieldBearingInfo.maxExposure = xFeeMint[length - 2]; uint64[] memory xFeeBurn; - (xFeeBurn, ) = TRANSMUTER.getCollateralBurnFees(collatInfo.asset); + (xFeeBurn, ) = transmuter.getCollateralBurnFees(asset); length = xFeeBurn.length; - if (length <= 1) collatInfo.minExposureYieldAsset = 0; - else collatInfo.minExposureYieldAsset = xFeeBurn[length - 2]; + if (length <= 1) yieldBearingInfo.minExposure = 0; + else yieldBearingInfo.minExposure = xFeeBurn[length - 2]; + } + + function _setMaxSlippage(uint96 newMaxSlippage) internal virtual { + if (newMaxSlippage > 1e9) revert InvalidParam(); + 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 + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////*/ + + 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/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/GenericHarvester.sol b/contracts/helpers/GenericHarvester.sol new file mode 100644 index 00000000..f3e9d07a --- /dev/null +++ b/contracts/helpers/GenericHarvester.sol @@ -0,0 +1,289 @@ +// 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 { 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 { 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"; +import { IERC4626 } from "interfaces/external/IERC4626.sol"; +import { BaseHarvester, YieldBearingParams } from "./BaseHarvester.sol"; + +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 BaseHarvester, IERC3156FlashBorrower, RouterSwapper { + 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 Budget of AGToken available for each users + mapping(address => uint256) public budget; + + /*////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + INITIALIZATION + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////*/ + + constructor( + uint96 initialMaxSlippage, + address initialTokenTransferAddress, + address initialSwapRouter, + IAgToken definitiveAgToken, + ITransmuter definitiveTransmuter, + IAccessControlManager definitiveAccessControlManager, + IERC3156FlashLender definitiveFlashloan + ) + RouterSwapper(initialSwapRouter, initialTokenTransferAddress) + BaseHarvester(initialMaxSlippage, definitiveAccessControlManager, definitiveAgToken, definitiveTransmuter) + { + if (address(definitiveFlashloan) == address(0)) revert ZeroAddress(); + flashloan = definitiveFlashloan; + + IERC20(agToken).safeApprove(address(definitiveFlashloan), type(uint256).max); + } + + /*////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + 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 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, + /// 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; + if (amount == 0) revert ZeroAmount(); + + (SwapType swapType, bytes memory data) = abi.decode(extraData, (SwapType, bytes)); + 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 + /// 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 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 asset, + uint256 minAmountOut, + SwapType swapType, + bytes memory extraData + ) public virtual { + flashloan.flashLoan( + IERC3156FlashBorrower(address(this)), + address(agToken), + amountStablecoins, + abi.encode(msg.sender, increase, yieldBearingAsset, asset, minAmountOut, swapType, 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; + uint256 minAmountOut; + SwapType swapType; + bytes memory callData; + address tokenOut; + address tokenIn; + { + address yieldBearingAsset; + 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 = asset; + } else { + // Decrease yield exposure action: we bring in the deposit asset + tokenIn = yieldBearingAsset; + tokenOut = asset; + } + } + uint256 amountOut = transmuter.swapExactInput( + amount, + 0, + address(agToken), + tokenIn, + address(this), + block.timestamp + ); + + // Swap to tokenIn + amountOut = _swapToTokenOut(typeAction, tokenIn, tokenOut, amountOut, swapType, callData); + + _adjustAllowance(tokenOut, address(transmuter), amountOut); + uint256 amountStableOut = transmuter.swapExactInput( + amountOut, + minAmountOut, + tokenOut, + address(agToken), + address(this), + block.timestamp + ); + if (amount > amountStableOut) { + budget[sender] -= amount - amountStableOut; // Will revert if not enough funds + } + return CALLBACK_SUCCESS; + } + + /*////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + 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); + } + + /*////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + INTERNALS + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////*/ + + function _swapToTokenOut( + uint256 typeAction, + address tokenIn, + address tokenOut, + uint256 amount, + SwapType swapType, + bytes memory callData + ) internal returns (uint256 amountOut) { + if (swapType == SwapType.SWAP) { + amountOut = _swapToTokenOutSwap(tokenIn, tokenOut, amount, callData); + } else if (swapType == SwapType.VAULT) { + amountOut = _swapToTokenOutVault(typeAction, tokenIn, tokenOut, amount); + } + } + + /** + * @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 _swapToTokenOutSwap( + address tokenIn, + address tokenOut, + uint256 amount, + bytes memory callData + ) internal returns (uint256) { + uint256 balance = IERC20(tokenOut).balanceOf(address(this)); + + address[] memory tokens = new address[](1); + 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); + + 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(tokenIn, tokenOut, amount); + amountOut = IERC4626(tokenOut).deposit(amount, address(this)); + } else amountOut = IERC4626(tokenIn).redeem(amount, address(this), address(this)); + } +} 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/MultiBlockHarvester.sol b/contracts/helpers/MultiBlockHarvester.sol new file mode 100644 index 00000000..fe663364 --- /dev/null +++ b/contracts/helpers/MultiBlockHarvester.sol @@ -0,0 +1,183 @@ +// SPDX-License-Identifier: GPL-3.0 + +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 { 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"; +import { IAgToken } from "../interfaces/IAgToken.sol"; +import { IPool } from "../interfaces/IPool.sol"; + +import "../utils/Errors.sol"; +import "../utils/Constants.sol"; + +/// @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; + + /*////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + VARIABLES + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////*/ + + /// @notice address to deposit to receive yieldBearingAsset + mapping(address => address) public yieldBearingToDepositAddress; + + /*////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + CONSTRUCTOR + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////*/ + + constructor( + uint96 initialMaxSlippage, + IAccessControlManager definitiveAccessControlManager, + IAgToken definitiveAgToken, + ITransmuter definitiveTransmuter + ) BaseHarvester(initialMaxSlippage, definitiveAccessControlManager, definitiveAgToken, definitiveTransmuter) {} + + /*////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + GUARDIAN FUNCTIONS + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////*/ + + /** + * @notice Set the deposit address for a yieldBearingAsset + * @param yieldBearingAsset address of the yieldBearingAsset + * @param newDepositAddress address to deposit to receive yieldBearingAsset + */ + function setYieldBearingToDepositAddress( + address yieldBearingAsset, + address newDepositAddress + ) external onlyGuardian { + yieldBearingToDepositAddress[yieldBearingAsset] = newDepositAddress; + } + + /*////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + TRUSTED FUNCTIONS + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////*/ + + /** + * @notice Initiate a rebalance + * @param scale scale to apply to the rebalance amount + * @param yieldBearingAsset address of the yieldBearingAsset + */ + 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); + amount = (amount * scale) / 1e9; + if (amount == 0) revert ZeroAmount(); + + try transmuter.updateOracle(yieldBearingAsset) {} catch {} + _rebalance(increase, yieldBearingAsset, yieldBearingInfo, amount); + } + + /** + * @notice Finalize a rebalance + * @param yieldBearingAsset address of the yieldBearingAsset + */ + function finalizeRebalance(address yieldBearingAsset, uint256 balance) external onlyTrusted { + try transmuter.updateOracle(yieldBearingAsset) {} catch {} + _adjustAllowance(yieldBearingAsset, address(transmuter), balance); + uint256 amountOut = transmuter.swapExactInput( + balance, + 0, + yieldBearingAsset, + address(agToken), + address(this), + block.timestamp + ); + address depositAddress = yieldBearingAsset == XEVT + ? yieldBearingToDepositAddress[yieldBearingAsset] + : address(0); + _checkSlippage(balance, amountOut, yieldBearingAsset, depositAddress, true); + } + + /*////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + INTERNAL FUNCTIONS + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////*/ + + function _rebalance( + uint8 typeAction, + address yieldBearingAsset, + YieldBearingParams memory yieldBearingInfo, + uint256 amount + ) internal { + _adjustAllowance(address(agToken), address(transmuter), amount); + address depositAddress = yieldBearingToDepositAddress[yieldBearingAsset]; + if (typeAction == 1) { + uint256 amountOut = transmuter.swapExactInput( + amount, + 0, + address(agToken), + yieldBearingInfo.asset, + address(this), + block.timestamp + ); + _checkSlippage(amount, amountOut, yieldBearingInfo.asset, depositAddress, false); + if (yieldBearingAsset == XEVT) { + _adjustAllowance(yieldBearingInfo.asset, address(depositAddress), amountOut); + (uint256 shares, ) = IPool(depositAddress).deposit(amountOut, address(this)); + _adjustAllowance(yieldBearingAsset, address(transmuter), shares); + amountOut = transmuter.swapExactInput( + shares, + 0, + yieldBearingAsset, + address(agToken), + address(this), + block.timestamp + ); + } else if (yieldBearingAsset == USDM) { + IERC20(yieldBearingInfo.asset).safeTransfer(depositAddress, amountOut); + } + } else { + uint256 amountOut = transmuter.swapExactInput( + amount, + 0, + address(agToken), + yieldBearingAsset, + address(this), + block.timestamp + ); + _checkSlippage(amount, amountOut, yieldBearingAsset, depositAddress, false); + if (yieldBearingAsset == XEVT) { + IPool(depositAddress).requestRedeem(amountOut); + } else if (yieldBearingAsset == USDM) { + IERC20(yieldBearingAsset).safeTransfer(depositAddress, amountOut); + } + } + } + + /*////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + HELPER + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////*/ + + 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 + amountIn = _scaleAmountBasedOnDecimals(decimalsAsset, 18, amountIn, assetIn); + + if (asset == USDC || asset == USDM || asset == EURC) { + // Assume 1:1 ratio between stablecoins + 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 + unchecked { + uint256 slippage = ((IPool(depositAddress).convertToAssets(amountIn) - amountOut) * 1e9) / amountIn; + if (slippage > maxSlippage) revert SlippageTooHigh(); + } + } else revert InvalidParam(); + } +} 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/contracts/interfaces/IHarvester.sol b/contracts/interfaces/IHarvester.sol new file mode 100644 index 00000000..3cc01066 --- /dev/null +++ b/contracts/interfaces/IHarvester.sol @@ -0,0 +1,20 @@ +// SPDX-License-Identifier: GPL-3.0 + +pragma solidity >=0.5.0; + +interface IHarvester { + 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; +} diff --git a/contracts/interfaces/IPool.sol b/contracts/interfaces/IPool.sol new file mode 100644 index 00000000..a9bc9bf0 --- /dev/null +++ b/contracts/interfaces/IPool.sol @@ -0,0 +1,13 @@ +// 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); + + function convertToAssets(uint256 shares) external view returns (uint256 assets); + + function convertToShares(uint256 assets) external view returns (uint256 shares); +} 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; -} 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/contracts/utils/Constants.sol b/contracts/utils/Constants.sol index 34fcdd83..f288ffb9 100644 --- a/contracts/utils/Constants.sol +++ b/contracts/utils/Constants.sol @@ -62,3 +62,10 @@ 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 STEAK_USDC = 0xBEEF01735c132Ada46AA9aA4c54623cAA92A64CB; +address constant USDC = 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48; +address constant EURC = 0x1aBaEA1f7C830bD89Acc67eC4af516284b1bC33c; + +address constant ONEINCH_ROUTER = 0x111111125421cA6dc452d289314280a0f8842A65; diff --git a/contracts/utils/Errors.sol b/contracts/utils/Errors.sol index 855c8099..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(); @@ -44,3 +45,4 @@ error ZeroAddress(); error ZeroAmount(); error SwapError(); error SlippageTooHigh(); +error InsufficientFunds(); 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 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..6c72c068 --- /dev/null +++ b/scripts/DeployGenericHarvester.s.sol @@ -0,0 +1,39 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity ^0.8.19; + +import "./utils/Utils.s.sol"; +import { console } from "forge-std/console.sol"; +import { 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); + 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)); + IAccessControlManager accessControlManager = transmuter.accessControlManager(); + + GenericHarvester harvester = new GenericHarvester( + maxSlippage, + ONEINCH_ROUTER, + ONEINCH_ROUTER, + 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/DeployMultiBlockHarvester.s.sol b/scripts/DeployMultiBlockHarvester.s.sol new file mode 100644 index 00000000..da93d300 --- /dev/null +++ b/scripts/DeployMultiBlockHarvester.s.sol @@ -0,0 +1,34 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity ^0.8.19; + +import "./utils/Utils.s.sol"; +import { console } from "forge-std/console.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 DeployMultiBlockHarvester 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); + 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(); + + MultiBlockHarvester harvester = new MultiBlockHarvester( + 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; diff --git a/test/fuzz/GenericHarvester.t.sol b/test/fuzz/GenericHarvester.t.sol new file mode 100644 index 00000000..bb505e58 --- /dev/null +++ b/test/fuzz/GenericHarvester.t.sol @@ -0,0 +1,285 @@ +// 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 "../mock/MockRouter.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, + 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.tokenTransferAddress(), ONEINCH_ROUTER); + 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(); + + vm.expectRevert(Errors.NotGovernorOrGuardian.selector); + harvester.recoverERC20(USDC, 1e12, alice); + } + + 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_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))); + } + + 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_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); + + uint256 beforeBudget = harvester.budget(alice); + + (uint8 expectedIncrease, uint256 expectedAmount) = harvester.computeRebalanceAmount(STEAK_USDC); + assertEq(expectedIncrease, 0); + + 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(); + } + + 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 newTargetExposure) internal { + vm.prank(governor); + harvester.setYieldBearingAssetData( + yieldBearingAsset, + stablecoin, + newTargetExposure, + minExposureYieldAsset, + maxExposureYieldAsset, + 1 + ); + } +} 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..113441af --- /dev/null +++ b/test/fuzz/MultiBlockHarvester.t.sol @@ -0,0 +1,597 @@ +// 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 "../mock/MockScaleDecimals.sol"; +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"; + +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", 21_041_434); + + // 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(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) to transfer XEVT + vm.mockCall( + 0x9019Fd383E490B4B045130707C9A1227F36F4636, + abi.encodeWithSelector(IXEVT.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((90 * 1e9) / 100); + minExposureYieldAsset = uint64((5 * 1e9) / 100); + + harvester = new MultiBlockHarvester(1e8, accessControlManager, agToken, transmuter); + vm.startPrank(governor); + harvester.toggleTrusted(alice); + harvester.setYieldBearingToDepositAddress(XEVT, XEVT); + harvester.setYieldBearingToDepositAddress(USDM, receiver); + + agToken.mint(address(harvester), 1_000_000e18); + + 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(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.toggleTrusted(alice); + + vm.expectRevert(Errors.NotGovernorOrGuardian.selector); + harvester.recoverERC20(USDC, 1e12, 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); + 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 { + 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(EURC), xFeeBurn, yFeeBurn, false); + transmuter.setFees(address(EURC), xFeeMint, yFeeMint, true); + harvester.setYieldBearingAssetData( + address(XEVT), + address(EURC), + targetExposure, + minExposureYieldAsset, + maxExposureYieldAsset, + 0 + ); + harvester.updateLimitExposuresYieldAsset(address(XEVT)); + + (, , uint64 maxi, uint64 mini, ) = harvester.yieldBearingData(address(XEVT)); + assertEq(maxi, (15 * BASE_9) / 100); + assertEq(mini, BASE_9 / 10); + vm.stopPrank(); + } + + function test_ToggleTrusted() public { + vm.startPrank(governor); + harvester.toggleTrusted(bob); + assertEq(harvester.isTrusted(bob), true); + + harvester.toggleTrusted(bob); + assertEq(harvester.isTrusted(bob), false); + + 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); + (, uint64 currentTargetExposure, , , ) = harvester.yieldBearingData(address(EURC)); + 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); + _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); + + (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 { + 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 + + // fake semd EURC to harvester + deal(EURC, address(harvester), amount); + + vm.prank(alice); + harvester.finalizeRebalance(EURC, amount); + + 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 { + 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); + + // fake semd USDC to harvester + deal(USDC, address(harvester), amount); + + vm.prank(alice); + harvester.finalizeRebalance(USDC, amount); + + 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 { + 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); + + // fake semd USDC to harvester + deal(USDC, address(harvester), amount); + + vm.prank(alice); + harvester.finalizeRebalance(USDC, amount); + + 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_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 + } + + 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); + } +} 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/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); + } +} 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(); - } -} 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;