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
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
11 changes: 9 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@ jobs:
with:
registry-token: ${{ secrets.GH_REGISTRY_ACCESS_TOKEN }}

- name: Install dependencies
run: yarn install

- name: Run solhint
run: yarn lint:check

Expand All @@ -51,6 +54,9 @@ jobs:
with:
registry-token: ${{ secrets.GH_REGISTRY_ACCESS_TOKEN }}

- name: Install dependencies
run: yarn install --frozen-lockfile

- name: Compile foundry
run: yarn compile --sizes

Expand Down Expand Up @@ -224,12 +230,13 @@ jobs:
run: forge build --build-info --skip */test/** */scripts/** --force

- name: "Run Slither analysis"
uses: "crytic/[email protected].0"
uses: "crytic/[email protected].2"
id: "slither"
with:
ignore-compile: true
fail-on: "none"
sarif: "results.sarif"
slither-version: "0.10.1"

- name: "Upload SARIF file to GitHub code scanning"
uses: "github/codeql-action/upload-sarif@v2"
Expand All @@ -239,4 +246,4 @@ jobs:
- name: "Add Slither summary"
run: |
echo "## Slither result" >> $GITHUB_STEP_SUMMARY
echo "✅ Uploaded to GitHub code scanning" >> $GITHUB_STEP_SUMMARY
echo "✅ Uploaded to GitHub code scanning" >> $GITHUB_STEP_SUMMARY
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ export
typechain
slither-audit.txt
slither
cache

# Test output
coverage
Expand Down
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(
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) {
// Increase yield exposure action: we bring in the ERC4626 token
tokenOut = collateral;
tokenIn = vault;
} else {
// Decrease yield exposure action: we bring in the liquid asset
tokenIn = collateral;
tokenOut = vault;
}
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));
_adjustAllowance(tokenIn, address(TRANSMUTER), amountOut);
uint256 amountStableOut = TRANSMUTER.swapExactInput(
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);
}
return CALLBACK_SUCCESS;
}
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();
}
}
1 change: 0 additions & 1 deletion scripts/UpdateTransmuterFacets.s.sol
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ import "contracts/transmuter/libraries/LibHelpers.sol";
import { ITransmuter } from "interfaces/ITransmuter.sol";
import "utils/src/Constants.sol";
import { IERC20 } from "oz/interfaces/IERC20.sol";
import { OldTransmuter } from "test/scripts/UpdateTransmuterFacets.t.sol";

contract UpdateTransmuterFacets is Helpers {
string[] replaceFacetNames;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@ import { stdJson } from "forge-std/StdJson.sol";
import { console } from "forge-std/console.sol";
import { Test } from "forge-std/Test.sol";

import "../../scripts/Constants.s.sol";
import "../Constants.s.sol";

import { Helpers } from "../../scripts/Helpers.s.sol";
import { Helpers } from "../Helpers.s.sol";
import "contracts/utils/Errors.sol" as Errors;
import "contracts/transmuter/Storage.sol" as Storage;
import { Getters } from "contracts/transmuter/facets/Getters.sol";
Expand Down Expand Up @@ -53,7 +53,7 @@ contract UpdateTransmuterFacetsTest is Helpers, Test {
CHAIN_SOURCE = CHAIN_ETHEREUM;

ethereumFork = vm.createFork(vm.envString("ETH_NODE_URI_MAINNET"), 19425035);
vm.selectFork(forkIdentifier[CHAIN_SOURCE]);
vm.selectFork(ethereumFork);

governor = _chainToContract(CHAIN_SOURCE, ContractType.Timelock);
transmuter = ITransmuter(_chainToContract(CHAIN_SOURCE, ContractType.TransmuterAgEUR));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@ import { stdJson } from "forge-std/StdJson.sol";
import { console } from "forge-std/console.sol";
import { Test } from "forge-std/Test.sol";

import "../../scripts/Constants.s.sol";
import "../Constants.s.sol";

import { Helpers } from "../../scripts/Helpers.s.sol";
import { Helpers } from "../Helpers.s.sol";
import "contracts/utils/Errors.sol" as Errors;
import "contracts/transmuter/Storage.sol" as Storage;
import { Getters } from "contracts/transmuter/facets/Getters.sol";
Expand All @@ -21,7 +21,7 @@ import { CollateralSetupProd } from "contracts/transmuter/configs/ProductionType
import { ITransmuter } from "interfaces/ITransmuter.sol";
import "utils/src/Constants.sol";
import { IERC20 } from "oz/interfaces/IERC20.sol";
import { IMorphoOracle, MockMorphoOracle } from "../mock/MockMorphoOracle.sol";
import { IMorphoOracle, MockMorphoOracle } from "../../test/mock/MockMorphoOracle.sol";

interface OldTransmuter {
function getOracle(
Expand Down Expand Up @@ -54,7 +54,7 @@ contract UpdateTransmuterFacetsUSDATest is Helpers, Test {
CHAIN_SOURCE = CHAIN_ETHEREUM;

ethereumFork = vm.createFork(vm.envString("ETH_NODE_URI_MAINNET"), 19483530);
vm.selectFork(forkIdentifier[CHAIN_SOURCE]);
vm.selectFork(ethereumFork);

governor = DEPLOYER;
transmuter = ITransmuter(0x222222fD79264BBE280b4986F6FEfBC3524d0137);
Expand Down
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