Skip to content

Commit

Permalink
Feat/harvester-swap (#116)
Browse files Browse the repository at this point in the history
* feat: Harvester and Balancer with swap logic

* fix: forwardUtils now sends error

* feat: use Swapper from utils

* refactor: remove duplicated code

* fix: correct asset and collateral for HarvesterVault

* fix: handle slippage with different decimals

* feat: scale of harvested amount

* tests: all rebalancer and harvester swap tests

* fix: correct slippage handling for RebalancerSwap

* style: remove lint issues

* feat: abstract onFlashLoan

* fix: script updateFacets tests
  • Loading branch information
0xtekgrinder authored Sep 3, 2024
1 parent d592dd9 commit 1c417c4
Show file tree
Hide file tree
Showing 29 changed files with 1,118 additions and 192 deletions.
2 changes: 1 addition & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
},
"slither.solcPath": "",
"slither.hiddenDetectors": [],
"solidity.compileUsingRemoteVersion": "v0.8.22",
"solidity.compileUsingRemoteVersion": "v0.8.23",
"files.insertFinalNewline": true,
"solidity.remappings": [
"ds-test/=lib/forge-std/lib/ds-test/src/",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,6 @@

pragma solidity ^0.8.23;

import { IERC20 } from "oz/interfaces/IERC20.sol";
import { IERC4626 } from "interfaces/external/IERC4626.sol";
import { SafeERC20 } from "oz/token/ERC20/utils/SafeERC20.sol";
import { SafeCast } from "oz/utils/math/SafeCast.sol";

import { ITransmuter } from "interfaces/ITransmuter.sol";
Expand All @@ -13,34 +10,32 @@ import { AccessControl, IAccessControlManager } from "../utils/AccessControl.sol
import "../utils/Constants.sol";
import "../utils/Errors.sol";

import { RebalancerFlashloan } from "./RebalancerFlashloan.sol";
import { IRebalancerFlashloan } from "../interfaces/IRebalancerFlashloan.sol";

struct CollatParams {
// Vault associated to the collateral
address vault;
// Target exposure to the collateral asset used in the vault
// Yield bearing asset associated to the collateral
address asset;
// Target exposure to the collateral asset used
uint64 targetExposure;
// Maximum exposure within the Transmuter to the vault asset
// Maximum exposure within the Transmuter to the asset
uint64 maxExposureYieldAsset;
// Minimum exposure within the Transmuter to the vault asset
// Minimum exposure within the Transmuter to the asset
uint64 minExposureYieldAsset;
// Whether limit exposures should be overriden or read onchain through the Transmuter
// This value should be 1 to override exposures or 2 if these shouldn't be overriden
uint64 overrideExposures;
}

/// @title Harvester
/// @title BaseHarvester
/// @author Angle Labs, Inc.
/// @dev Contract for anyone to permissionlessly adjust the reserves of Angle Transmuter through
/// the RebalancerFlashloan contract
contract Harvester is AccessControl {
using SafeERC20 for IERC20;
/// @dev Generic contract for anyone to permissionlessly adjust the reserves of Angle Transmuter through
contract BaseHarvester is AccessControl {
using SafeCast for uint256;

/// @notice Reference to the `transmuter` implementation this contract aims at rebalancing
ITransmuter public immutable TRANSMUTER;
/// @notice Permissioned rebalancer contract
RebalancerFlashloan public rebalancer;
IRebalancerFlashloan public rebalancer;
/// @notice Max slippage when dealing with the Transmuter
uint96 public maxSlippage;
/// @notice Data associated to a collateral
Expand All @@ -52,18 +47,26 @@ contract Harvester is AccessControl {

constructor(
address _rebalancer,
address vault,
address collateral,
address asset,
uint64 targetExposure,
uint64 overrideExposures,
uint64 maxExposureYieldAsset,
uint64 minExposureYieldAsset,
uint96 _maxSlippage
) {
ITransmuter transmuter = RebalancerFlashloan(_rebalancer).TRANSMUTER();
ITransmuter transmuter = IRebalancerFlashloan(_rebalancer).TRANSMUTER();
TRANSMUTER = transmuter;
rebalancer = RebalancerFlashloan(_rebalancer);
rebalancer = IRebalancerFlashloan(_rebalancer);
accessControlManager = IAccessControlManager(transmuter.accessControlManager());
_setCollateralData(vault, targetExposure, minExposureYieldAsset, maxExposureYieldAsset, overrideExposures);
_setCollateralData(
collateral,
asset,
targetExposure,
minExposureYieldAsset,
maxExposureYieldAsset,
overrideExposures
);
_setMaxSlippage(_maxSlippage);
}

Expand All @@ -77,12 +80,11 @@ contract Harvester is AccessControl {
/// that can then be used for people looking to burn stablecoins
/// @dev Due to potential transaction fees within the Transmuter, this function doesn't exactly bring `collateral`
/// to the target exposure
/// @dev The `harvest` possibility shouldn't be implemented for assets with a manipulable price (like ERC4626)
/// contracts on which the `previewRedeem` values can be easily moved by creating a loss or a profit
function harvest(address collateral) external {
function harvest(address collateral, uint256 scale, bytes calldata extraData) public virtual {
if (scale > 1e9) revert InvalidParam();
(uint256 stablecoinsFromCollateral, uint256 stablecoinsIssued) = TRANSMUTER.getIssuedByCollateral(collateral);
CollatParams memory collatInfo = collateralData[collateral];
(uint256 stablecoinsFromVault, ) = TRANSMUTER.getIssuedByCollateral(collatInfo.vault);
(uint256 stablecoinsFromAsset, ) = TRANSMUTER.getIssuedByCollateral(collatInfo.asset);
uint8 increase;
uint256 amount;
uint256 targetExposureScaled = collatInfo.targetExposure * stablecoinsIssued;
Expand All @@ -93,27 +95,29 @@ contract Harvester is AccessControl {
uint256 maxValueScaled = collatInfo.maxExposureYieldAsset * stablecoinsIssued;
// These checks assume that there are no transaction fees on the stablecoin->collateral conversion and so
// it's still possible that exposure goes above the max exposure in some rare cases
if (stablecoinsFromVault * 1e9 > maxValueScaled) amount = 0;
else if ((stablecoinsFromVault + amount) * 1e9 > maxValueScaled)
amount = maxValueScaled / 1e9 - stablecoinsFromVault;
if (stablecoinsFromAsset * 1e9 > maxValueScaled) amount = 0;
else if ((stablecoinsFromAsset + amount) * 1e9 > maxValueScaled)
amount = maxValueScaled / 1e9 - stablecoinsFromAsset;
} else {
// In this case, exposure after the operation might remain slightly below the targetExposure as less
// collateral may be obtained by burning stablecoins for the yield asset and unwrapping it
amount = targetExposureScaled / 1e9 - stablecoinsFromCollateral;
uint256 minValueScaled = collatInfo.minExposureYieldAsset * stablecoinsIssued;
if (stablecoinsFromVault * 1e9 < minValueScaled) amount = 0;
else if (stablecoinsFromVault * 1e9 < minValueScaled + amount * 1e9)
amount = stablecoinsFromVault - minValueScaled / 1e9;
if (stablecoinsFromAsset * 1e9 < minValueScaled) amount = 0;
else if (stablecoinsFromAsset * 1e9 < minValueScaled + amount * 1e9)
amount = stablecoinsFromAsset - minValueScaled / 1e9;
}
amount = (amount * scale) / 1e9;
if (amount > 0) {
try TRANSMUTER.updateOracle(collatInfo.vault) {} catch {}
try TRANSMUTER.updateOracle(collatInfo.asset) {} catch {}

rebalancer.adjustYieldExposure(
amount,
increase,
collateral,
collatInfo.vault,
(amount * (1e9 - maxSlippage)) / 1e9
collatInfo.asset,
(amount * (1e9 - maxSlippage)) / 1e9,
extraData
);
}
}
Expand All @@ -122,47 +126,53 @@ contract Harvester is AccessControl {
SETTERS
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////*/

function setRebalancer(address _newRebalancer) external onlyGuardian {
function setRebalancer(address _newRebalancer) public virtual onlyGuardian {
if (_newRebalancer == address(0)) revert ZeroAddress();
rebalancer = RebalancerFlashloan(_newRebalancer);
rebalancer = IRebalancerFlashloan(_newRebalancer);
}

/// @dev This function shouldn't be called for a vault (e.g an ERC4626 token) which price can be easily moved
/// by creating a loss or a profit, at the risk of depleting the reserves available in the Rebalancer
function setCollateralData(
address vault,
address collateral,
address asset,
uint64 targetExposure,
uint64 minExposureYieldAsset,
uint64 maxExposureYieldAsset,
uint64 overrideExposures
) external onlyGuardian {
_setCollateralData(vault, targetExposure, minExposureYieldAsset, maxExposureYieldAsset, overrideExposures);
) public virtual onlyGuardian {
_setCollateralData(
collateral,
asset,
targetExposure,
minExposureYieldAsset,
maxExposureYieldAsset,
overrideExposures
);
}

function setMaxSlippage(uint96 _maxSlippage) external onlyGuardian {
function setMaxSlippage(uint96 _maxSlippage) public virtual onlyGuardian {
_setMaxSlippage(_maxSlippage);
}

function updateLimitExposuresYieldAsset(address collateral) external {
function updateLimitExposuresYieldAsset(address collateral) public virtual {
CollatParams storage collatInfo = collateralData[collateral];
if (collatInfo.overrideExposures == 2) _updateLimitExposuresYieldAsset(collatInfo);
}

function _setMaxSlippage(uint96 _maxSlippage) internal {
function _setMaxSlippage(uint96 _maxSlippage) internal virtual {
if (_maxSlippage > 1e9) revert InvalidParam();
maxSlippage = _maxSlippage;
}

function _setCollateralData(
address vault,
address collateral,
address asset,
uint64 targetExposure,
uint64 minExposureYieldAsset,
uint64 maxExposureYieldAsset,
uint64 overrideExposures
) internal {
address collateral = address(IERC4626(vault).asset());
) internal virtual {
CollatParams storage collatInfo = collateralData[collateral];
collatInfo.vault = vault;
collatInfo.asset = asset;
if (targetExposure >= 1e9) revert InvalidParam();
collatInfo.targetExposure = targetExposure;
collatInfo.overrideExposures = overrideExposures;
Expand All @@ -176,15 +186,15 @@ contract Harvester is AccessControl {
}
}

function _updateLimitExposuresYieldAsset(CollatParams storage collatInfo) internal {
function _updateLimitExposuresYieldAsset(CollatParams storage collatInfo) internal virtual {
uint64[] memory xFeeMint;
(xFeeMint, ) = TRANSMUTER.getCollateralMintFees(collatInfo.vault);
(xFeeMint, ) = TRANSMUTER.getCollateralMintFees(collatInfo.asset);
uint256 length = xFeeMint.length;
if (length <= 1) collatInfo.maxExposureYieldAsset = 1e9;
else collatInfo.maxExposureYieldAsset = xFeeMint[length - 2];

uint64[] memory xFeeBurn;
(xFeeBurn, ) = TRANSMUTER.getCollateralBurnFees(collatInfo.vault);
(xFeeBurn, ) = TRANSMUTER.getCollateralBurnFees(collatInfo.asset);
length = xFeeBurn.length;
if (length <= 1) collatInfo.minExposureYieldAsset = 0;
else collatInfo.minExposureYieldAsset = xFeeBurn[length - 2];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,13 @@
pragma solidity ^0.8.19;

import "./Rebalancer.sol";
import { IERC4626 } from "interfaces/external/IERC4626.sol";
import { IERC3156FlashBorrower } from "oz/interfaces/IERC3156FlashBorrower.sol";
import { IERC3156FlashLender } from "oz/interfaces/IERC3156FlashLender.sol";

/// @title RebalancerFlashloan
/// @title BaseRebalancerFlashloan
/// @author Angle Labs, Inc.
/// @dev Rebalancer contract for a Transmuter with as collaterals a liquid stablecoin and an ERC4626 token
/// using this liquid stablecoin as an asset
contract RebalancerFlashloan is Rebalancer, IERC3156FlashBorrower {
/// @dev General rebalancer contract with flashloan capabilities
contract BaseRebalancerFlashloan is Rebalancer, IERC3156FlashBorrower {
using SafeERC20 for IERC20;
using SafeCast for uint256;
bytes32 public constant CALLBACK_SUCCESS = keccak256("ERC3156FlashBorrower.onFlashLoan");
Expand All @@ -29,24 +27,26 @@ contract RebalancerFlashloan is Rebalancer, IERC3156FlashBorrower {
IERC20(AGTOKEN).safeApprove(address(_flashloan), type(uint256).max);
}

/// @notice Burns `amountStablecoins` for one collateral asset and mints stablecoins from the proceeds of the
/// first burn
/// @notice Burns `amountStablecoins` for one collateral asset, swap for asset then mints stablecoins
/// from the proceeds of the swap.
/// @dev If `increase` is 1, then the system tries to increase its exposure to the yield bearing asset which means
/// burning stablecoin for the liquid asset, depositing into the ERC4626 vault, then minting the stablecoin
/// burning stablecoin for the liquid asset, swapping for the yield bearing asset, then minting the stablecoin
/// @dev This function reverts if the second stablecoin mint gives less than `minAmountOut` of stablecoins
/// @dev This function reverts if the swap slippage is higher than `maxSlippage`
function adjustYieldExposure(
uint256 amountStablecoins,
uint8 increase,
address collateral,
address vault,
uint256 minAmountOut
) external {
address asset,
uint256 minAmountOut,
bytes calldata extraData
) public virtual {
if (!TRANSMUTER.isTrustedSeller(msg.sender)) revert NotTrusted();
FLASHLOAN.flashLoan(
IERC3156FlashBorrower(address(this)),
address(AGTOKEN),
amountStablecoins,
abi.encode(increase, collateral, vault, minAmountOut)
abi.encode(increase, collateral, asset, minAmountOut, extraData)
);
}

Expand All @@ -57,29 +57,26 @@ contract RebalancerFlashloan is Rebalancer, IERC3156FlashBorrower {
uint256 amount,
uint256 fee,
bytes calldata data
) external returns (bytes32) {
) public virtual returns (bytes32) {
if (msg.sender != address(FLASHLOAN) || initiator != address(this) || fee != 0) revert NotTrusted();
(uint256 typeAction, address collateral, address vault, uint256 minAmountOut) = abi.decode(
data,
(uint256, address, address, uint256)
);
(uint256 typeAction, address collateral, address asset, uint256 minAmountOut, bytes memory callData) = abi
.decode(data, (uint256, address, address, uint256, bytes));
address tokenOut;
address tokenIn;
if (typeAction == 1) {
// Increase yield exposure action: we bring in the ERC4626 token
// Increase yield exposure action: we bring in the yield bearing asset
tokenOut = collateral;
tokenIn = vault;
tokenIn = asset;
} else {
// Decrease yield exposure action: we bring in the liquid asset
tokenIn = collateral;
tokenOut = vault;
tokenOut = asset;
}
uint256 amountOut = TRANSMUTER.swapExactInput(amount, 0, AGTOKEN, tokenOut, address(this), block.timestamp);
if (typeAction == 1) {
// Granting allowance with the collateral for the vault asset
_adjustAllowance(collateral, vault, amountOut);
amountOut = IERC4626(vault).deposit(amountOut, address(this));
} else amountOut = IERC4626(vault).redeem(amountOut, address(this), address(this));

// Swap to tokenIn
amountOut = _swapToTokenIn(typeAction, tokenIn, tokenOut, amountOut, callData);

_adjustAllowance(tokenIn, address(TRANSMUTER), amountOut);
uint256 amountStableOut = TRANSMUTER.swapExactInput(
amountOut,
Expand All @@ -97,4 +94,20 @@ contract RebalancerFlashloan is Rebalancer, IERC3156FlashBorrower {
}
return CALLBACK_SUCCESS;
}

/**
* @dev hook to swap from tokenOut to tokenIn
* @param typeAction 1 for deposit, 2 for redeem
* @param tokenIn address of the token to swap
* @param tokenOut address of the token to receive
* @param amount amount of token to swap
* @param callData extra call data (if needed)
*/
function _swapToTokenIn(
uint256 typeAction,
address tokenIn,
address tokenOut,
uint256 amount,
bytes memory callData
) internal virtual returns (uint256) {}
}
33 changes: 33 additions & 0 deletions contracts/helpers/HarvesterSwap.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
// SPDX-License-Identifier: GPL-3.0

pragma solidity ^0.8.23;

import "./BaseHarvester.sol";

/// @title HarvesterSwap
/// @author Angle Labs, Inc.
/// @dev Contract for anyone to permissionlessly adjust the reserves of Angle Transmuter through
/// the RebalancerFlashloanSwap contract
contract HarvesterSwap is BaseHarvester {
constructor(
address _rebalancer,
address collateral,
address asset,
uint64 targetExposure,
uint64 overrideExposures,
uint64 maxExposureYieldAsset,
uint64 minExposureYieldAsset,
uint96 _maxSlippage
)
BaseHarvester(
_rebalancer,
collateral,
asset,
targetExposure,
overrideExposures,
maxExposureYieldAsset,
minExposureYieldAsset,
_maxSlippage
)
{}
}
Loading

0 comments on commit 1c417c4

Please sign in to comment.