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/generic harvester #117

Merged
merged 69 commits into from
Oct 29, 2024
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
Show all changes
69 commits
Select commit Hold shift + click to select a range
4956509
feat: first version of generic harvester
0xtekgrinder Sep 13, 2024
513009b
feat: generic harvester swap and vault
0xtekgrinder Sep 13, 2024
455ea99
feat: remove access control from adjustYield
0xtekgrinder Sep 13, 2024
8b0a95d
feat: merge genericHarvester into a single contract
0xtekgrinder Sep 16, 2024
f620502
feat: multi block rebalancer
0xtekgrinder Sep 17, 2024
875665f
feat: correct permissions for MultiMockRebalancer
0xtekgrinder Sep 17, 2024
857caee
feat: slippage handling for MultiblockRebalancer
0xtekgrinder Sep 17, 2024
919971a
feat: remove old harvesters
0xtekgrinder Sep 20, 2024
cd1e1b4
feat: correct slippage computation for the vault
0xtekgrinder Sep 20, 2024
737b96c
feat: refactory harvesters to abstract common code
0xtekgrinder Sep 20, 2024
b7e1a89
feat: deploy scripts for new harvesters
0xtekgrinder Sep 20, 2024
9afc214
style: format BaseRebalancer
0xtekgrinder Sep 20, 2024
6580fdd
feat: remove comment
0xtekgrinder Sep 20, 2024
c2c6d06
chore: remove useless file
0xtekgrinder Sep 20, 2024
7b96168
fix: correct computation of slippage for XEVT
0xtekgrinder Sep 20, 2024
c3da3ef
feat: specify balance in finalizeRebalance function
0xtekgrinder Sep 20, 2024
88210b9
refactor: rename swap arguments
0xtekgrinder Sep 20, 2024
a4f4b85
refactor: change mapping of stablecoin to yieldbearing to the oposite
0xtekgrinder Sep 23, 2024
00ee738
feat: harvest function for both harvester contracts
0xtekgrinder Sep 23, 2024
6c3da10
feat: checkSlippage with decimals
0xtekgrinder Sep 23, 2024
5454e28
chore: remove old rebalancer interfaces
0xtekgrinder Sep 23, 2024
dfddaa3
style: fix lint issues for the new contracts
0xtekgrinder Sep 23, 2024
ef574fc
feat: remove useless setYieldBearingAssetData function override
0xtekgrinder Sep 29, 2024
4dd5a4f
feat: correct computation of the rebalancing
0xtekgrinder Oct 7, 2024
d8c9aa1
fix: correct sleeping check + toggleTrusted
0xtekgrinder Oct 7, 2024
3eb3da7
TEMP feat: tests
0xtekgrinder Oct 8, 2024
7b0d455
feat: computeRebalanceAmount public function
0xtekgrinder Oct 8, 2024
36d22ea
fix: correct rebalancing computation + min/max for stablecoin
0xtekgrinder Oct 8, 2024
e01866c
tests: add more complete tests for the harvesting of the different as…
0xtekgrinder Oct 8, 2024
3fc3e83
refactor: rename minExposure / maxExposure to be more explicit
0xtekgrinder Oct 8, 2024
378f35f
feat: check for msg.sender instead of receiver when removing budget
0xtekgrinder Oct 15, 2024
35132a8
doc: add some explanation on scale
0xtekgrinder Oct 15, 2024
8abc893
feat: system for allowed addresses to update the target exposure of a…
0xtekgrinder Oct 21, 2024
7c417ff
doc: finalize sentance
0xtekgrinder Oct 21, 2024
3e11a2b
doc: misspell
0xtekgrinder Oct 21, 2024
9a10225
feat: remove underflow checks
0xtekgrinder Oct 21, 2024
427eaf5
feat: make updateLimitExposure permissionless
0xtekgrinder Oct 21, 2024
dea0f6c
feat: internalize scaling of amount
0xtekgrinder Oct 21, 2024
4aa4cdc
feat: only allowed or guardian
0xtekgrinder Oct 21, 2024
0ba6215
fix: update state before sending tokens in addBudget
0xtekgrinder Oct 22, 2024
4b701a8
style: refactor blocks for GenericHarvester
0xtekgrinder Oct 22, 2024
e1c67a7
fix: better slippage handling for vaults
0xtekgrinder Oct 22, 2024
f9e9a5f
feat: IXEVT interface
0xtekgrinder Oct 22, 2024
d473b84
feat: refactor access control so there is only one onlyTrusted mapping
0xtekgrinder Oct 22, 2024
3bc5b0c
style: fix lint of comments
0xtekgrinder Oct 22, 2024
bcda179
tests: update tests to add missing functionnalities
0xtekgrinder Oct 22, 2024
2af51df
feat: remove mint and burn from MultiBlockHarvester
0xtekgrinder Oct 25, 2024
29df833
doc: typo
0xtekgrinder Oct 25, 2024
496dda8
feat: declare depositAddress in the upper scope
0xtekgrinder Oct 25, 2024
cf6b776
feat: simplify rebalance
0xtekgrinder Oct 25, 2024
29b7a5a
feat: specify block fork number
0xtekgrinder Oct 25, 2024
2b28e0f
refactor: rename stablecoin to depositAsset
0xtekgrinder Oct 25, 2024
791763f
feat: add unchecked blocks to _checkSlippage
0xtekgrinder Oct 25, 2024
cddfd4b
refactor: rename depositAsset to asset
0xtekgrinder Oct 25, 2024
7a39fc9
feat: remove MaxOrderAmount check
0xtekgrinder Oct 25, 2024
bfeac9e
tests: remove tooBigAmountIn
0xtekgrinder Oct 25, 2024
8146026
tests: setup of generic harvester + budget
0xtekgrinder Oct 25, 2024
08d8a28
tests: setters
0xtekgrinder Oct 25, 2024
e7fd84f
feat: revert if zero amount for GenericHarvester
0xtekgrinder Oct 25, 2024
2d0c3d2
fix: correct generic harvester swaps
0xtekgrinder Oct 28, 2024
1139a9b
tests: add not enough budget test
0xtekgrinder Oct 28, 2024
094f958
feat: remove swap slippage
0xtekgrinder Oct 28, 2024
bf111b9
tests: increase and decrease steak_USDC exposure
0xtekgrinder Oct 28, 2024
e303ae0
feat: remove swapSlippage variable
0xtekgrinder Oct 28, 2024
0fc36d0
feat: set maxSlippage to 0.3%
0xtekgrinder Oct 29, 2024
c85bfb8
tests: add generic harvester swap test
0xtekgrinder Oct 29, 2024
b05e00c
tests: add decimals tests
0xtekgrinder Oct 29, 2024
671eb76
feat: recover ERC20s
0xtekgrinder Oct 29, 2024
e5d7e51
feat: events for harvester
0xtekgrinder Oct 29, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
336 changes: 336 additions & 0 deletions contracts/harvester/GenericHarvester.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,336 @@
// SPDX-License-Identifier: GPL-3.0

pragma solidity ^0.8.23;

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

import { ITransmuter } from "interfaces/ITransmuter.sol";

import { AccessControl, IAccessControlManager } from "../utils/AccessControl.sol";
import "../utils/Constants.sol";
import "../utils/Errors.sol";

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

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

/// @title GenericHarvester
/// @author Angle Labs, Inc.
/// @dev Generic contract for anyone to permissionlessly adjust the reserves of Angle Transmuter
contract GenericHarvester is AccessControl, IERC3156FlashBorrower {
using SafeCast for uint256;
using SafeERC20 for IERC20;
bytes32 public constant CALLBACK_SUCCESS = keccak256("ERC3156FlashBorrower.onFlashLoan");

/// @notice Angle stablecoin flashloan contract
IERC3156FlashLender public immutable FLASHLOAN;

/// @notice Reference to the `transmuter` implementation this contract aims at rebalancing
ITransmuter public immutable TRANSMUTER;
/// @notice AgToken handled by the `transmuter` of interest
address public immutable AGTOKEN;
/// @notice Max slippage when dealing with the Transmuter
uint96 public maxSlippage;
/// @notice Data associated to a collateral
mapping(address => CollatParams) public collateralData;
/// @notice Budget of AGToken available for each users
mapping(address => uint256) public budget;

/*//////////////////////////////////////////////////////////////////////////////////////////////////////////////////
INITIALIZATION
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////*/

constructor(
address transmuter,
address collateral,
address asset,
address flashloan,
uint64 targetExposure,
uint64 overrideExposures,
uint64 maxExposureYieldAsset,
uint64 minExposureYieldAsset,
uint96 _maxSlippage
) {
if (flashloan == address(0)) revert ZeroAddress();
FLASHLOAN = IERC3156FlashLender(flashloan);
TRANSMUTER = ITransmuter(transmuter);
AGTOKEN = address(ITransmuter(transmuter).agToken());

IERC20(AGTOKEN).safeApprove(flashloan, type(uint256).max);
accessControlManager = IAccessControlManager(ITransmuter(transmuter).accessControlManager());
_setCollateralData(
collateral,
asset,
targetExposure,
minExposureYieldAsset,
maxExposureYieldAsset,
overrideExposures
);
_setMaxSlippage(_maxSlippage);
}

/*//////////////////////////////////////////////////////////////////////////////////////////////////////////////////
REBALANCE
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////*/

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

/// @inheritdoc IERC3156FlashBorrower
function onFlashLoan(
address initiator,
address,
uint256 amount,
uint256 fee,
bytes calldata data
) public virtual returns (bytes32) {
if (msg.sender != address(FLASHLOAN) || initiator != address(this) || fee != 0) revert NotTrusted();
(
address sender,
uint256 typeAction,
address collateral,
address asset,
uint256 minAmountOut,
bytes memory callData
) = abi.decode(data, (address, uint256, address, address, uint256, bytes));
address tokenOut;
address tokenIn;
if (typeAction == 1) {
// Increase yield exposure action: we bring in the yield bearing asset
tokenOut = collateral;
tokenIn = asset;
} else {
// Decrease yield exposure action: we bring in the liquid asset
tokenIn = collateral;
tokenOut = asset;
}
uint256 amountOut = TRANSMUTER.swapExactInput(amount, 0, AGTOKEN, tokenOut, address(this), block.timestamp);

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

_adjustAllowance(tokenIn, address(TRANSMUTER), amountOut);
uint256 amountStableOut = TRANSMUTER.swapExactInput(
amountOut,
minAmountOut,
tokenIn,
AGTOKEN,
address(this),
block.timestamp
);
if (amount > amountStableOut) {
// TODO temporary fix for for subsidy as stack too deep
if (budget[sender] < amount - amountStableOut) revert InsufficientFunds();
budget[sender] -= amount - amountStableOut;
}
return CALLBACK_SUCCESS;
}

Check notice

Code scanning / Slither

Block timestamp Low


/**
* @notice Add budget to a receiver
* @param amount amount of AGToken to add to the budget
* @param receiver address of the receiver
*/
function addBudget(uint256 amount, address receiver) public virtual {
IERC20(AGTOKEN).safeTransferFrom(msg.sender, address(this), amount);

budget[receiver] += amount;
}

Check notice

Code scanning / Slither

Reentrancy vulnerabilities Low


/**
* @notice Remove budget from a receiver
* @param amount amount of AGToken to remove from the budget
* @param receiver address of the receiver
*/
function removeBudget(uint256 amount, address receiver) public virtual {
if (budget[receiver] < amount) revert InsufficientFunds();
budget[receiver] -= amount;

IERC20(AGTOKEN).safeTransfer(receiver, amount);
}

/**
* @dev hook to swap from tokenOut to tokenIn
* @param typeAction 1 for deposit, 2 for redeem
* @param tokenIn address of the token to swap
* @param tokenOut address of the token to receive
* @param amount amount of token to swap
* @param callData extra call data (if needed)
*/
function _swapToTokenIn(
uint256 typeAction,
address tokenIn,
address tokenOut,
uint256 amount,
bytes memory callData
) internal virtual returns (uint256) {}

Check warning

Code scanning / Slither

Dead-code Warning


/*//////////////////////////////////////////////////////////////////////////////////////////////////////////////////
HARVEST
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////*/

/// @notice Invests or divests from the yield asset associated to `collateral` based on the current exposure to this
/// collateral
/// @dev This transaction either reduces the exposure to `collateral` in the Transmuter or frees up some collateral
/// that can then be used for people looking to burn stablecoins
/// @dev Due to potential transaction fees within the Transmuter, this function doesn't exactly bring `collateral`
/// to the target exposure
function harvest(address collateral, uint256 scale, bytes calldata extraData) public virtual {
if (scale > 1e9) revert InvalidParam();
(uint256 stablecoinsFromCollateral, uint256 stablecoinsIssued) = TRANSMUTER.getIssuedByCollateral(collateral);
CollatParams memory collatInfo = collateralData[collateral];
(uint256 stablecoinsFromAsset, ) = TRANSMUTER.getIssuedByCollateral(collatInfo.asset);
uint8 increase;

Check warning

Code scanning / Slither

Uninitialized local variables Medium

GenericHarvester.harvest(address,uint256,bytes).increase is a local variable never initialized
uint256 amount;
uint256 targetExposureScaled = collatInfo.targetExposure * stablecoinsIssued;
if (stablecoinsFromCollateral * 1e9 > targetExposureScaled) {
// Need to increase exposure to yield bearing asset
increase = 1;
amount = stablecoinsFromCollateral - targetExposureScaled / 1e9;
uint256 maxValueScaled = collatInfo.maxExposureYieldAsset * stablecoinsIssued;
// These checks assume that there are no transaction fees on the stablecoin->collateral conversion and so
// it's still possible that exposure goes above the max exposure in some rare cases
if (stablecoinsFromAsset * 1e9 > maxValueScaled) amount = 0;
else if ((stablecoinsFromAsset + amount) * 1e9 > maxValueScaled)
amount = maxValueScaled / 1e9 - stablecoinsFromAsset;
} else {
// In this case, exposure after the operation might remain slightly below the targetExposure as less
// collateral may be obtained by burning stablecoins for the yield asset and unwrapping it
amount = targetExposureScaled / 1e9 - stablecoinsFromCollateral;
uint256 minValueScaled = collatInfo.minExposureYieldAsset * stablecoinsIssued;
if (stablecoinsFromAsset * 1e9 < minValueScaled) amount = 0;
else if (stablecoinsFromAsset * 1e9 < minValueScaled + amount * 1e9)
amount = stablecoinsFromAsset - minValueScaled / 1e9;
}
amount = (amount * scale) / 1e9;
if (amount > 0) {
try TRANSMUTER.updateOracle(collatInfo.asset) {} catch {}

adjustYieldExposure(
amount,
increase,
collateral,
collatInfo.asset,
(amount * (1e9 - maxSlippage)) / 1e9,
extraData
);
}
}

Check warning

Code scanning / Slither

Divide before multiply Medium

Check warning

Code scanning / Slither

Unused return Medium


/*//////////////////////////////////////////////////////////////////////////////////////////////////////////////////
SETTERS
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////*/

function setCollateralData(
address collateral,
address asset,
uint64 targetExposure,
uint64 minExposureYieldAsset,
uint64 maxExposureYieldAsset,
uint64 overrideExposures
) public virtual onlyGuardian {
_setCollateralData(
collateral,
asset,
targetExposure,
minExposureYieldAsset,
maxExposureYieldAsset,
overrideExposures
);
}

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

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

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

function _setCollateralData(
address collateral,
address asset,
uint64 targetExposure,
uint64 minExposureYieldAsset,
uint64 maxExposureYieldAsset,
uint64 overrideExposures
) internal virtual {
CollatParams storage collatInfo = collateralData[collateral];
collatInfo.asset = asset;
if (targetExposure >= 1e9) revert InvalidParam();
collatInfo.targetExposure = targetExposure;
collatInfo.overrideExposures = overrideExposures;
if (overrideExposures == 1) {
if (maxExposureYieldAsset >= 1e9 || minExposureYieldAsset >= maxExposureYieldAsset) revert InvalidParam();
collatInfo.maxExposureYieldAsset = maxExposureYieldAsset;
collatInfo.minExposureYieldAsset = minExposureYieldAsset;
} else {
collatInfo.overrideExposures = 2;
_updateLimitExposuresYieldAsset(collatInfo);
}
}

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

uint64[] memory xFeeBurn;
(xFeeBurn, ) = TRANSMUTER.getCollateralBurnFees(collatInfo.asset);
length = xFeeBurn.length;
if (length <= 1) collatInfo.minExposureYieldAsset = 0;
else collatInfo.minExposureYieldAsset = xFeeBurn[length - 2];
}

/*//////////////////////////////////////////////////////////////////////////////////////////////////////////////////
HELPER
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////*/

function _adjustAllowance(address token, address sender, uint256 amountIn) internal {
uint256 allowance = IERC20(token).allowance(address(this), sender);
if (allowance < amountIn) IERC20(token).safeIncreaseAllowance(sender, type(uint256).max - allowance);
}
}
Loading
Loading