Skip to content

Commit

Permalink
feat: Flashloan rebalancer (#112)
Browse files Browse the repository at this point in the history
* updating rebalancer

* simplify tests

* feat: add fuzz back

* feat: add new test for minAmount

* feat: add deployment script

* Empty-Commit

* tests: comment out upgrade tests of the transmuter and remove helpers

* chore: add cache to folders to ignore

* chore: fix slither ci infinite loop

* chore: setup repo before slither ci

---------

Co-authored-by: 0xtekgrinder <[email protected]>
  • Loading branch information
sogipec and 0xtekgrinder authored Apr 12, 2024
1 parent ec7d7ab commit 6aecf1a
Show file tree
Hide file tree
Showing 11 changed files with 590 additions and 28 deletions.
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;
}
}
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

0 comments on commit 6aecf1a

Please sign in to comment.