Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat/harvester-swap #116

Merged
merged 12 commits into from
Sep 3, 2024
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 "../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 @@

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,92 +80,99 @@
/// 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;
if (stablecoinsFromCollateral * 1e9 > targetExposureScaled) {
// Need to increase exposure to yield bearing asset
increase = 1;
amount = stablecoinsFromCollateral - targetExposureScaled / 1e9;
uint256 maxValueScaled = collatInfo.maxExposureYieldAsset * stablecoinsIssued;
// These checks assume that there are no transaction fees on the stablecoin->collateral conversion and so
// it's still possible that exposure goes above the max exposure in some rare cases
if (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
);
}
}

Check warning

Code scanning / Slither

Unused return Medium


/*//////////////////////////////////////////////////////////////////////////////////////////////////////////////////
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,17 +186,17 @@
}
}

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];
}

Check warning

Code scanning / Slither

Unused return Medium

Check warning

Code scanning / Slither

Unused return Medium

}
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 @@
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 @@
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);
0xtekgrinder marked this conversation as resolved.
Show resolved Hide resolved

_adjustAllowance(tokenIn, address(TRANSMUTER), amountOut);
uint256 amountStableOut = TRANSMUTER.swapExactInput(
amountOut,
Expand All @@ -97,4 +94,20 @@
}
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) {}
Comment on lines +106 to +112

Check warning

Code scanning / Slither

Dead-code Warning

}
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
0xtekgrinder marked this conversation as resolved.
Show resolved Hide resolved

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,
Fixed Show fixed Hide fixed
Fixed Show fixed Hide fixed
Fixed Show fixed Hide fixed
Fixed Show fixed Hide fixed
uint64 minExposureYieldAsset,
uint96 _maxSlippage
)
BaseHarvester(
_rebalancer,
collateral,
asset,
targetExposure,
overrideExposures,
maxExposureYieldAsset,
minExposureYieldAsset,
_maxSlippage
)
{}
}
Loading
Loading