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: Flashloan rebalancer #112

Merged
merged 10 commits into from
Apr 12, 2024
13 changes: 10 additions & 3 deletions contracts/helpers/Rebalancer.sol
Original file line number Diff line number Diff line change
Expand Up @@ -70,9 +70,7 @@ contract Rebalancer is IRebalancer, AccessControl {
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
uint256 allowance = IERC20(tokenIn).allowance(address(this), address(TRANSMUTER));
if (allowance < amountIn)
IERC20(tokenIn).safeIncreaseAllowance(address(TRANSMUTER), type(uint256).max - allowance);
_adjustAllowance(tokenIn, address(TRANSMUTER), amountIn);
// Mint agToken from `tokenIn`
uint256 amountAgToken = TRANSMUTER.swapExactInput(
amountIn,
Expand Down Expand Up @@ -199,4 +197,13 @@ contract Rebalancer is IRebalancer, AccessControl {
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);
}
}
100 changes: 100 additions & 0 deletions contracts/helpers/RebalancerFlashloan.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
// SPDX-License-Identifier: GPL-3.0

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
/// @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 {
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 and mints stablecoins from the proceeds of the
/// first burn
/// @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
/// @dev This function reverts if the second stablecoin mint gives less than `minAmountOut` of stablecoins
function adjustYieldExposure(
uint256 amountStablecoins,
uint8 increase,
address collateral,
address vault,
uint256 minAmountOut
) external {
if (!TRANSMUTER.isTrustedSeller(msg.sender)) revert NotTrusted();
FLASHLOAN.flashLoan(

Check warning on line 45 in contracts/helpers/RebalancerFlashloan.sol

View check run for this annotation

Codecov / codecov/patch

contracts/helpers/RebalancerFlashloan.sol#L45

Added line #L45 was not covered by tests
IERC3156FlashBorrower(address(this)),
address(AGTOKEN),
amountStablecoins,
abi.encode(increase, collateral, vault, minAmountOut)
);
}

/// @inheritdoc IERC3156FlashBorrower
function onFlashLoan(
address initiator,
address,
uint256 amount,
uint256 fee,
bytes calldata data
) external 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)
);
address tokenOut;
address tokenIn;
if (typeAction == 1) {

Check warning on line 68 in contracts/helpers/RebalancerFlashloan.sol

View check run for this annotation

Codecov / codecov/patch

contracts/helpers/RebalancerFlashloan.sol#L66-L68

Added lines #L66 - L68 were not covered by tests
// Increase yield exposure action: we bring in the ERC4626 token
tokenOut = collateral;
tokenIn = vault;

Check warning on line 71 in contracts/helpers/RebalancerFlashloan.sol

View check run for this annotation

Codecov / codecov/patch

contracts/helpers/RebalancerFlashloan.sol#L70-L71

Added lines #L70 - L71 were not covered by tests
} else {
// Decrease yield exposure action: we bring in the liquid asset
tokenIn = collateral;
tokenOut = vault;

Check warning on line 75 in contracts/helpers/RebalancerFlashloan.sol

View check run for this annotation

Codecov / codecov/patch

contracts/helpers/RebalancerFlashloan.sol#L74-L75

Added lines #L74 - L75 were not covered by tests
}
uint256 amountOut = TRANSMUTER.swapExactInput(amount, 0, AGTOKEN, tokenOut, address(this), block.timestamp);
if (typeAction == 1) {

Check warning on line 78 in contracts/helpers/RebalancerFlashloan.sol

View check run for this annotation

Codecov / codecov/patch

contracts/helpers/RebalancerFlashloan.sol#L77-L78

Added lines #L77 - L78 were not covered by tests
// 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));
_adjustAllowance(tokenIn, address(TRANSMUTER), amountOut);
uint256 amountStableOut = TRANSMUTER.swapExactInput(

Check warning on line 84 in contracts/helpers/RebalancerFlashloan.sol

View check run for this annotation

Codecov / codecov/patch

contracts/helpers/RebalancerFlashloan.sol#L80-L84

Added lines #L80 - L84 were not covered by tests
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);

Check warning on line 96 in contracts/helpers/RebalancerFlashloan.sol

View check run for this annotation

Codecov / codecov/patch

contracts/helpers/RebalancerFlashloan.sol#L92-L96

Added lines #L92 - L96 were not covered by tests
}
return CALLBACK_SUCCESS;

Check warning on line 98 in contracts/helpers/RebalancerFlashloan.sol

View check run for this annotation

Codecov / codecov/patch

contracts/helpers/RebalancerFlashloan.sol#L98

Added line #L98 was not covered by tests
}
Comment on lines +54 to +99

Check notice

Code scanning / Slither

Block timestamp Low

}
39 changes: 39 additions & 0 deletions scripts/DeployRebalancerFlashloan.s.sol
Original file line number Diff line number Diff line change
@@ -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 { RebalancerFlashloan } from "contracts/helpers/RebalancerFlashloan.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 DeployRebalancerFlashloan 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))));
RebalancerFlashloan rebalancer = new RebalancerFlashloan(
IAccessControlManager(_chainToContract(CHAIN_SOURCE, ContractType.CoreBorrow)),
ITransmuter(_chainToContract(CHAIN_SOURCE, ContractType.TransmuterAgUSD)),
IERC3156FlashLender(0x4A2FF9bC686A0A23DA13B6194C69939189506F7F)
);
/*
RebalancerFlashloan rebalancer = new RebalancerFlashloan(
IAccessControlManager(0x5bc6BEf80DA563EBf6Df6D6913513fa9A7ec89BE),
ITransmuter(0x222222fD79264BBE280b4986F6FEfBC3524d0137),
IERC3156FlashLender(0x4A2FF9bC686A0A23DA13B6194C69939189506F7F)
);
*/
console.log("Rebalancer deployed at: ", address(rebalancer));

vm.stopBroadcast();
}
}
28 changes: 13 additions & 15 deletions test/fuzz/OracleTest.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -983,21 +983,19 @@ contract OracleTest is Fixture, FunctionUtils {
}

function _getReadType(uint8 newReadType) internal pure returns (Storage.OracleReadType readType) {
readType = newReadType == 0
? Storage.OracleReadType.CHAINLINK_FEEDS
: newReadType == 1
? Storage.OracleReadType.EXTERNAL
: newReadType == 2
? Storage.OracleReadType.NO_ORACLE
: newReadType == 3
? Storage.OracleReadType.STABLE
: newReadType == 4
? Storage.OracleReadType.WSTETH
: newReadType == 5
? Storage.OracleReadType.CBETH
: newReadType == 6
? Storage.OracleReadType.RETH
: Storage.OracleReadType.SFRXETH;
readType = newReadType == 0 ? Storage.OracleReadType.CHAINLINK_FEEDS : newReadType == 1
? Storage.OracleReadType.EXTERNAL
: newReadType == 2
? Storage.OracleReadType.NO_ORACLE
: newReadType == 3
? Storage.OracleReadType.STABLE
: newReadType == 4
? Storage.OracleReadType.WSTETH
: newReadType == 5
? Storage.OracleReadType.CBETH
: newReadType == 6
? Storage.OracleReadType.RETH
: Storage.OracleReadType.SFRXETH;
}

function _updateOracles(
Expand Down
89 changes: 89 additions & 0 deletions test/fuzz/RebalancerFlashloanTest.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
// 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";

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

RebalancerFlashloan 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 RebalancerFlashloan(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 RebalancerFlashloan(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);
}

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();
}
}
Loading
Loading