Skip to content

Commit

Permalink
finishing harvesting tests
Browse files Browse the repository at this point in the history
  • Loading branch information
sogipec committed May 24, 2024
1 parent 2e08235 commit e1e9ea2
Show file tree
Hide file tree
Showing 3 changed files with 506 additions and 29 deletions.
65 changes: 36 additions & 29 deletions contracts/helpers/Harvester.sol
Original file line number Diff line number Diff line change
Expand Up @@ -15,52 +15,54 @@ import "../utils/Errors.sol";

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

struct CollatParams {
// Vault associated to the collateral
address vault;
// Target exposure to the collateral asset used in the vault
uint64 targetExposure;
// Maximum exposure within the Transmuter to the vault asset
uint64 maxExposureYieldAsset;
// Minimum exposure within the Transmuter to the vault asset
uint64 minExposureYieldAsset;
// Whether limit exposures should be overriden or read onchain through the Transmuter
uint64 overrideExposures;
}

/// @title Harvester
/// @author Angle Labs, Inc.
/// @dev Contract for anyone to permissionlessly adjust the reserves of Angle Transmuter through the RebalancerFlashloan contract

Check failure on line 33 in contracts/helpers/Harvester.sol

View workflow job for this annotation

GitHub Actions / lint

Line length must be no more than 120 but current length is 129
contract Harvester is AccessControl {
using SafeERC20 for IERC20;
using SafeCast for uint256;

struct CollateralSetup {
// Vault associated to the collateral
address vault;
// Target exposure to the collateral asset used in the vault
uint64 targetExposure;
// Maximum exposure within the Transmuter to the vault asset
uint64 maxExposureYieldAsset;
// Minimum exposure within the Transmuter to the vault asset
uint64 minExposureYieldAsset;
// Whether limit exposures should be overriden or read onchain through the Transmuter
uint64 overrideExposures;
}

/// @notice Reference to the `transmuter` implementation this contract aims at rebalancing
ITransmuter public immutable TRANSMUTER;
/// @notice Permissioned rebalancer contract
RebalancerFlashloan public rebalancer;
/// @notice Max slippage when dealing with the Transmuter
uint96 public maxSlippage;
/// @notice Data associated to a collateral
mapping(address => CollateralSetup) public collateralData;
mapping(address => CollatParams) public collateralData;

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

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

/*//////////////////////////////////////////////////////////////////////////////////////////////////////////////////
Expand All @@ -74,15 +76,15 @@ contract Harvester is AccessControl {
/// to the target exposure
function harvest(address collateral) external {
(uint256 stablecoinsFromCollateral, uint256 stablecoinsIssued) = TRANSMUTER.getIssuedByCollateral(collateral);
CollateralSetup memory collatInfo = collateralData[collateral];
CollatParams memory collatInfo = collateralData[collateral];
(uint256 stablecoinsFromVault, ) = TRANSMUTER.getIssuedByCollateral(collatInfo.vault);
uint8 increase;
uint256 amount;
uint256 targetExposureLiquid = collatInfo.targetExposure;
if (stablecoinsFromCollateral * 1e9 > targetExposureLiquid * stablecoinsIssued) {
uint256 targetExposureScaled = collatInfo.targetExposure * stablecoinsIssued;
if (stablecoinsFromCollateral * 1e9 > targetExposureScaled) {
// Need to increase exposure to yield bearing asset
increase = 1;
amount = stablecoinsFromCollateral - (targetExposureLiquid * stablecoinsIssued) / 1e9;
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
Expand All @@ -92,14 +94,15 @@ contract Harvester 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 = (targetExposureLiquid * stablecoinsIssued) / 1e9 - stablecoinsFromCollateral;
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 (amount > 0) {
TRANSMUTER.updateOracle(collatInfo.vault);
try TRANSMUTER.updateOracle(collatInfo.vault) {} catch {}

rebalancer.adjustYieldExposure(
amount,
increase,
Expand Down Expand Up @@ -130,15 +133,19 @@ contract Harvester is AccessControl {
}

function setMaxSlippage(uint96 _maxSlippage) external onlyGuardian {
if (maxSlippage > 1e9) revert InvalidParam();
maxSlippage = _maxSlippage;
_setMaxSlippage(_maxSlippage);
}

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

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

function _setCollateralData(
address vault,
uint64 targetExposure,
Expand All @@ -147,7 +154,7 @@ contract Harvester is AccessControl {
uint64 overrideExposures
) internal {
address collateral = address(IERC4626(vault).asset());
CollateralSetup storage collatInfo = collateralData[collateral];
CollatParams storage collatInfo = collateralData[collateral];
collatInfo.vault = vault;
if (targetExposure >= 1e9) revert InvalidParam();
collatInfo.targetExposure = targetExposure;
Expand All @@ -161,7 +168,7 @@ contract Harvester is AccessControl {
}
}

function _updateLimitExposuresYieldAsset(CollateralSetup storage collatInfo) internal {
function _updateLimitExposuresYieldAsset(CollatParams storage collatInfo) internal {
uint64[] memory xFeeMint;
(xFeeMint, ) = TRANSMUTER.getCollateralMintFees(collatInfo.vault);
uint256 length = xFeeMint.length;
Expand Down
244 changes: 244 additions & 0 deletions test/fuzz/Harvester.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,244 @@
// 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/RebalancerFlashloan.sol";
import "contracts/helpers/Harvester.sol";

contract HarvesterTest is Fixture, FunctionUtils {
using SafeERC20 for IERC20;

RebalancerFlashloan public rebalancer;
Harvester public harvester;
Savings internal _saving;
address internal _savingImplementation;
string internal _name;
string internal _symbol;
address public collat;
uint64 public targetExposure;
uint64 public maxExposureYieldAsset;
uint64 public minExposureYieldAsset;

function setUp() public override {
super.setUp();

MockTokenPermit token = new MockTokenPermit("EURC", "EURC", 6);
collat = address(token);

_savingImplementation = address(new Savings());
bytes memory data;
_saving = Savings(_deployUpgradeable(address(proxyAdmin), _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();
targetExposure = uint64((15 * 1e9) / 100);
maxExposureYieldAsset = uint64((80 * 1e9) / 100);
minExposureYieldAsset = uint64((5 * 1e9) / 100);
rebalancer = new RebalancerFlashloan(accessControlManager, transmuter, IERC3156FlashLender(governor));
harvester = new Harvester(
address(rebalancer),
address(_saving),
targetExposure,
1,
maxExposureYieldAsset,
minExposureYieldAsset,
1e8
);
}

function test_RebalancerInitialization() public {
assertEq(address(harvester.rebalancer()), address(rebalancer));
assertEq(address(harvester.TRANSMUTER()), address(transmuter));
assertEq(address(harvester.accessControlManager()), address(accessControlManager));
(address vault, uint64 target, uint64 maxi, uint64 mini, uint64 overrideExp) = harvester.collateralData(collat);
assertEq(vault, address(_saving));
assertEq(target, targetExposure);
assertEq(maxi, maxExposureYieldAsset);
assertEq(mini, minExposureYieldAsset);
assertEq(overrideExp, 1);
}

function test_Constructor_RevertWhen_InvalidParams() public {
vm.expectRevert();
new Harvester(
address(0),
address(_saving),
targetExposure,
1,
maxExposureYieldAsset,
minExposureYieldAsset,
1e8
);

vm.expectRevert();
new Harvester(
address(rebalancer),
address(0),
targetExposure,
1,
maxExposureYieldAsset,
minExposureYieldAsset,
1e8
);

vm.expectRevert(Errors.InvalidParam.selector);
harvester = new Harvester(
address(rebalancer),
address(_saving),
1e10,
1,
maxExposureYieldAsset,
minExposureYieldAsset,
1e8
);

vm.expectRevert(Errors.InvalidParam.selector);
harvester = new Harvester(
address(rebalancer),
address(_saving),
1e10,
0,
maxExposureYieldAsset,
minExposureYieldAsset,
1e8
);

vm.expectRevert(Errors.InvalidParam.selector);
harvester = new Harvester(address(rebalancer), address(_saving), 1e9 / 10, 1, 1e10, minExposureYieldAsset, 1e8);

vm.expectRevert(Errors.InvalidParam.selector);
harvester = new Harvester(address(rebalancer), address(_saving), 1e9 / 10, 1, 1e8, 2e8, 1e8);

vm.expectRevert(Errors.InvalidParam.selector);
harvester = new Harvester(
address(rebalancer),
address(_saving),
targetExposure,
1,
maxExposureYieldAsset,
minExposureYieldAsset,
1e10
);
}

function test_OnlyGuardian_RevertWhen_NotGuardian() public {
vm.expectRevert(Errors.NotGovernorOrGuardian.selector);
harvester.setRebalancer(alice);

vm.expectRevert(Errors.NotGovernorOrGuardian.selector);
harvester.setCollateralData(address(_saving), targetExposure, 1, maxExposureYieldAsset, minExposureYieldAsset);

vm.expectRevert(Errors.NotGovernorOrGuardian.selector);
harvester.setMaxSlippage(1e9);

harvester.updateLimitExposuresYieldAsset(collat);
}

function test_SettersHarvester() public {
vm.startPrank(governor);
vm.expectRevert(Errors.InvalidParam.selector);
harvester.setMaxSlippage(1e10);

harvester.setMaxSlippage(123456);
assertEq(harvester.maxSlippage(), 123456);

vm.expectRevert(Errors.ZeroAddress.selector);
harvester.setRebalancer(address(0));

harvester.setRebalancer(address(harvester));
assertEq(address(harvester.rebalancer()), address(harvester));

harvester.setCollateralData(
address(_saving),
targetExposure + 10,
minExposureYieldAsset - 1,
maxExposureYieldAsset + 1,
1
);
(address vault, uint64 target, uint64 maxi, uint64 mini, uint64 overrideExp) = harvester.collateralData(collat);
assertEq(vault, address(_saving));
assertEq(target, targetExposure + 10);
assertEq(maxi, maxExposureYieldAsset + 1);
assertEq(mini, minExposureYieldAsset - 1);
assertEq(overrideExp, 1);

harvester.setCollateralData(
address(_saving),
targetExposure + 10,
minExposureYieldAsset - 1,
maxExposureYieldAsset + 1,
0
);
(vault, target, maxi, mini, overrideExp) = harvester.collateralData(collat);
assertEq(vault, address(_saving));
assertEq(target, targetExposure + 10);
assertEq(maxi, 1e9);
assertEq(mini, 0);
assertEq(overrideExp, 0);

vm.stopPrank();
}

function test_UpdateLimitExposuresYieldAsset() public {
bytes memory data;
Savings newVault = Savings(_deployUpgradeable(address(proxyAdmin), _savingImplementation, data));
_name = "savingAgEUR";
_symbol = "SAGEUR";

vm.startPrank(governor);
MockTokenPermit(address(eurA)).mint(governor, 1e12);
eurA.approve(address(newVault), 1e12);
newVault.initialize(accessControlManager, IERC20MetadataUpgradeable(address(eurA)), _name, _symbol, BASE_6);
transmuter.addCollateral(address(newVault));
vm.stopPrank();

uint64[] memory xFeeMint = new uint64[](3);
int64[] memory yFeeMint = new int64[](3);

xFeeMint[0] = 0;
xFeeMint[1] = uint64((15 * BASE_9) / 100);
xFeeMint[2] = uint64((2 * BASE_9) / 10);

yFeeMint[0] = int64(1);
yFeeMint[1] = int64(uint64(BASE_9 / 10));
yFeeMint[2] = int64(uint64((2 * BASE_9) / 10));

uint64[] memory xFeeBurn = new uint64[](3);
int64[] memory yFeeBurn = new int64[](3);

xFeeBurn[0] = uint64(BASE_9);
xFeeBurn[1] = uint64(BASE_9 / 10);
xFeeBurn[2] = 0;

yFeeBurn[0] = int64(1);
yFeeBurn[1] = int64(1);
yFeeBurn[2] = int64(uint64(BASE_9 / 10));

vm.startPrank(governor);
transmuter.setFees(address(newVault), xFeeBurn, yFeeBurn, false);
transmuter.setFees(address(newVault), xFeeMint, yFeeMint, true);
harvester.setCollateralData(address(newVault), targetExposure, minExposureYieldAsset, maxExposureYieldAsset, 0);
harvester.updateLimitExposuresYieldAsset(address(eurA));

(, , uint64 maxi, uint64 mini, ) = harvester.collateralData(address(eurA));
assertEq(maxi, (15 * BASE_9) / 100);
assertEq(mini, BASE_9 / 10);
vm.stopPrank();
}
}
Loading

0 comments on commit e1e9ea2

Please sign in to comment.