Skip to content

Commit

Permalink
chore: rebalancer review (#99)
Browse files Browse the repository at this point in the history
* chore: rebalancer review

* fixes

* fix: tests in the PR

* fix: setOrder function

* feat: deployment script

* adjust check on decimals

---------

Co-authored-by: Pablo Veyrat <[email protected]>
  • Loading branch information
Picodes and sogipec authored Dec 4, 2023
1 parent aa258b9 commit bae123f
Show file tree
Hide file tree
Showing 6 changed files with 171 additions and 71 deletions.
129 changes: 72 additions & 57 deletions contracts/helpers/Rebalancer.sol
Original file line number Diff line number Diff line change
Expand Up @@ -2,34 +2,35 @@

pragma solidity ^0.8.19;

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

Check warning on line 6 in contracts/helpers/Rebalancer.sol

View workflow job for this annotation

GitHub Actions / lint

imported name IERC20Metadata is not used
import { SafeERC20 } from "oz/token/ERC20/utils/SafeERC20.sol";
import { SafeCast } from "oz/utils/math/SafeCast.sol";

import { ITransmuter } from "interfaces/ITransmuter.sol";
import { Order, IRebalancer } from "interfaces/IRebalancer.sol";

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

/// @title Rebalancer
/// @author Angle Labs, Inc.
/// @notice Contract market makers building on top of Angle can use to rebalance the reserves of the protocol
contract Rebalancer is AccessControl {
using SafeERC20 for IERC20;
/// @notice Contract built to subsidize rebalances between collateral tokens
/// @dev This contract is meant to "wrap" the Transmuter contract and provide a way for governance to
/// subsidize rebalances between collateral tokens. Rebalances are done through 2 swaps collateral <> agToken.
/// @dev This contract is not meant to hold any transient funds aside from the rebalancing budget
contract Rebalancer is IRebalancer, AccessControl {
event OrderSet(address indexed tokenIn, address indexed tokenOut, uint256 subsidyBudget, uint256 guaranteedRate);
event SubsidyPaid(address indexed tokenIn, address indexed tokenOut, uint256 subsidy);

struct Order {
// Total agToken budget allocated to subsidize the swaps between the tokens associated to the order
uint256 subsidyBudget;
// Guaranteed exchange rate in `BASE_18` for the swaps between the `tokenIn` and `tokenOut` associated to
// the order. This rate is a minimum rate guaranteed up to when the subsidyBudget is fully consumed
uint256 guaranteedRate;
}
using SafeERC20 for IERC20;
using SafeCast for uint256;

/// @notice Reference to the `transmuter` implementation this contract aims at rebalancing
ITransmuter public immutable transmuter;
ITransmuter public immutable TRANSMUTER;
/// @notice AgToken handled by the `transmuter` of interest
address public immutable agToken;
address public immutable AGTOKEN;
/// @notice Maps a `(tokenIn,tokenOut)` pair to details about the subsidy potentially provided on
/// `tokenIn` to `tokenOut` rebalances
mapping(address tokenIn => mapping(address tokenOut => Order)) public orders;
Expand All @@ -40,28 +41,25 @@ contract Rebalancer is AccessControl {
INITIALIZATION
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////*/

/// @notice Initializes the immutable variables of the contract, namely `accessControlManager` and `transmuter`
/// @notice Initializes the immutable variables of the contract: `accessControlManager`, `transmuter` and `agToken`
constructor(IAccessControlManager _accessControlManager, ITransmuter _transmuter) {
if (address(_accessControlManager) == address(0)) revert ZeroAddress();
accessControlManager = _accessControlManager;
transmuter = _transmuter;
agToken = address(_transmuter.agToken());
TRANSMUTER = _transmuter;
AGTOKEN = address(_transmuter.agToken());
}

/*//////////////////////////////////////////////////////////////////////////////////////////////////////////////////
REBALANCING FUNCTIONS
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////*/

/// @notice Swaps `tokenIn` for `tokenOut` through an intermediary agToken mint from `tokenIn` and
/// burn to `tokenOut`. Eventually, this transaction may be sponsored and yield an amount of `tokenOut`
/// higher than what would be obtained through a mint and burn directly on the `transmuter`
/// @param amountIn Amount of `tokenIn` to bring for the rebalancing
/// @param amountOutMin Minimum amount of `tokenOut` that must be obtained from the swap
/// @param to Address to which `tokenOut` must be sent
/// @param deadline Timestamp before which this transaction must be included
/// @return amountOut Amount of outToken obtained
/// @inheritdoc IRebalancer
/// @dev Contrarily to what is done in the Transmuter contract, here neither of `tokenIn` or `tokenOut`
/// should be an `agToken`
/// @dev Can be used even if the subsidy budget is 0, in which case it'll just do 2 Transmuter swaps
/// @dev The invariant should be that `msg.sender` injects `amountIn` in the transmuter and either the
/// subsidy is 0 either they receive a subsidy from this contract on top of the output Transmuter up to
/// the guaranteed amount out
function swapExactInput(
uint256 amountIn,
uint256 amountOutMin,
Expand All @@ -73,61 +71,60 @@ contract Rebalancer is 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));
uint256 allowance = IERC20(tokenIn).allowance(address(this), address(TRANSMUTER));
if (allowance < amountIn)
IERC20(tokenIn).safeIncreaseAllowance(address(transmuter), type(uint256).max - allowance);
IERC20(tokenIn).safeIncreaseAllowance(address(TRANSMUTER), type(uint256).max - allowance);
// Mint agToken from `tokenIn`
uint256 amountAgToken = transmuter.swapExactInput(
uint256 amountAgToken = TRANSMUTER.swapExactInput(
amountIn,
0,
tokenIn,
agToken,
AGTOKEN,
address(this),
block.timestamp
);
// Computing if a potential subsidy must be included in the agToken amount to burn
uint256 subsidy = _getSubsidyAmount(tokenIn, tokenOut, amountAgToken, amountIn);
if (subsidy > 0) {
orders[tokenIn][tokenOut].subsidyBudget -= subsidy;
orders[tokenIn][tokenOut].subsidyBudget -= subsidy.toUint112();
budget -= subsidy;
amountAgToken += subsidy;

emit SubsidyPaid(tokenIn, tokenOut, subsidy);
}
amountOut = transmuter.swapExactInput(amountAgToken, amountOutMin, agToken, tokenOut, to, deadline);
amountOut = TRANSMUTER.swapExactInput(amountAgToken, amountOutMin, AGTOKEN, tokenOut, to, deadline);
}

/// @notice Approximates how much a call to `swapExactInput` with the same parameters would yield in terms
/// of `amountOut` and `subsidy`
/// @inheritdoc IRebalancer
/// @dev This function returns an approximation and not an exact value as the first mint to compute `amountAgToken`
/// might change the state of the fees slope within the Transmuter that will then be taken into account when
/// burning the minted agToken.
function quoteIn(uint256 amountIn, address tokenIn, address tokenOut) external view returns (uint256 amountOut) {
uint256 amountAgToken = transmuter.quoteIn(amountIn, tokenIn, agToken);
uint256 amountAgToken = TRANSMUTER.quoteIn(amountIn, tokenIn, AGTOKEN);
amountAgToken += _getSubsidyAmount(tokenIn, tokenOut, amountAgToken, amountIn);
amountOut = transmuter.quoteIn(amountAgToken, agToken, tokenOut);
amountOut = TRANSMUTER.quoteIn(amountAgToken, AGTOKEN, tokenOut);
}

/// @notice Helper to compute the minimum guaranteed amount out that would be obtained from a swap of `amountIn`
/// of `tokenIn` to `tokenOut`
/// @inheritdoc IRebalancer
/// @dev Note that this minimum amount is guaranteed up to the subsidy budget, and if for a swap the subsidy budget
/// is not big enough to provide this guaranteed amount out, then less will actually be obtained
function getGuaranteedAmountOut(
address tokenIn,
address tokenOut,
uint256 amountIn
) external view returns (uint256) {
return _getGuaranteedAmountOut(tokenIn, tokenOut, amountIn, orders[tokenIn][tokenOut].guaranteedRate);
Order storage order = orders[tokenIn][tokenOut];
return _getGuaranteedAmountOut(amountIn, order.guaranteedRate, order.decimalsIn, order.decimalsOut);
}

/// @notice Internal version of `_getGuaranteedAmountOut`
function _getGuaranteedAmountOut(
address tokenIn,
address tokenOut,
uint256 amountIn,
uint256 guaranteedRate
) internal view returns (uint256 amountOut) {
return
(amountIn * guaranteedRate * (10 ** IERC20Metadata(tokenOut).decimals())) /
(1e18 * (10 ** IERC20Metadata(tokenIn).decimals()));
uint256 guaranteedRate,
uint8 decimalsIn,
uint8 decimalsOut
) internal pure returns (uint256 amountOut) {
return (amountIn * guaranteedRate * (10 ** decimalsOut)) / (BASE_18 * (10 ** decimalsIn));
}

/// @notice Computes the additional subsidy amount in agToken that must be added during the process of a swap
Expand All @@ -139,14 +136,20 @@ contract Rebalancer is AccessControl {
uint256 amountIn
) internal view returns (uint256 subsidy) {
Order storage order = orders[tokenIn][tokenOut];
uint256 guaranteedAmountOut = _getGuaranteedAmountOut(tokenIn, tokenOut, amountIn, order.guaranteedRate);
uint256 guaranteedAmountOut = _getGuaranteedAmountOut(
amountIn,
order.guaranteedRate,
order.decimalsIn,
order.decimalsOut
);
// Computing the amount of agToken that must be burnt to get the amountOut guaranteed
if (guaranteedAmountOut > 0) {
uint256 amountAgTokenNeeded = transmuter.quoteOut(guaranteedAmountOut, agToken, tokenOut);
uint256 amountAgTokenNeeded = TRANSMUTER.quoteOut(guaranteedAmountOut, AGTOKEN, tokenOut);
// If more agTokens than what has been obtained through the first mint must be burnt to get to the
// guaranteed amountOut, we're taking it from the subsidy budget set
if (amountAgToken < amountAgTokenNeeded) {
subsidy = amountAgTokenNeeded - amountAgToken;

// In the case where the subsidy budget is too small, we may not be able to provide the guaranteed
// amountOut to the user
if (subsidy > order.subsidyBudget) subsidy = order.subsidyBudget;
Expand All @@ -158,30 +161,42 @@ contract Rebalancer is AccessControl {
GOVERNANCE
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////*/

/// @notice Lets governance set an order to subsidize rebalances between `tokenIn` and `tokenOut`
/// @inheritdoc IRebalancer
/// @dev Before calling this function, governance must make sure that there are enough `agToken` idle
/// in the contract to sponsor the swaps
/// @dev This function can be used to decrease an order by overriding it
function setOrder(
address tokenIn,
address tokenOut,
uint256 subsidyBudget,
uint256 guaranteedRate
) external onlyGuardian {
// If a token has 0 decimals on the Transmuter, then it's not an actual collateral
if (transmuter.getCollateralDecimals(tokenIn) == 0 || transmuter.getCollateralDecimals(tokenOut) == 0)
revert NotCollateral();
Order storage order = orders[tokenIn][tokenOut];
uint8 decimalsIn = order.decimalsIn;
uint8 decimalsOut = order.decimalsOut;
if (decimalsIn == 0) {
decimalsIn = TRANSMUTER.getCollateralDecimals(tokenIn);
order.decimalsIn = decimalsIn;
}
if (decimalsOut == 0) {
decimalsOut = TRANSMUTER.getCollateralDecimals(tokenOut);
order.decimalsOut = decimalsOut;
}
// If a token has 0 decimals on the Transmuter, then it's not an actual collateral of the Transmuter
if (decimalsIn == 0 || decimalsOut == 0) revert NotCollateral();
uint256 newBudget = budget + subsidyBudget - order.subsidyBudget;
if (IERC20(agToken).balanceOf(address(this)) < newBudget) revert InvalidParam();
if (IERC20(AGTOKEN).balanceOf(address(this)) < newBudget) revert InvalidParam();
budget = newBudget;
order.subsidyBudget = subsidyBudget;
order.guaranteedRate = guaranteedRate;
order.subsidyBudget = subsidyBudget.toUint112();
order.guaranteedRate = guaranteedRate.toUint128();

emit OrderSet(tokenIn, tokenOut, subsidyBudget, guaranteedRate);
}

/// @notice Recovers `amount` of `token` to the `to` address
/// @inheritdoc IRebalancer
/// @dev This function checks if too much is not being recovered with respect to currently available budgets
function recover(address token, uint256 amount, address to) external onlyGuardian {
if (token == address(agToken) && IERC20(token).balanceOf(address(this)) < budget + amount)
if (token == address(AGTOKEN) && IERC20(token).balanceOf(address(this)) < budget + amount)
revert InvalidParam();
IERC20(token).safeTransfer(to, amount);
}
Expand Down
55 changes: 55 additions & 0 deletions contracts/interfaces/IRebalancer.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
// SPDX-License-Identifier: GPL-3.0

pragma solidity >=0.5.0;

struct Order {
// Total agToken budget allocated to subsidize the swaps between the tokens associated to the order
uint112 subsidyBudget;
// Decimals of the `tokenIn` associated to the order
uint8 decimalsIn;
// Decimals of the `tokenOut` associated to the order
uint8 decimalsOut;
// Guaranteed exchange rate in `BASE_18` for the swaps between the `tokenIn` and `tokenOut` associated to
// the order. This rate is a minimum rate guaranteed up to when the `subsidyBudget` is fully consumed
uint128 guaranteedRate;
}

/// @title IRebalancer
/// @author Angle Labs, Inc.
interface IRebalancer {
/// @notice Swaps `tokenIn` for `tokenOut` through an intermediary agToken mint from `tokenIn` and
/// burn to `tokenOut`. Eventually, this transaction may be sponsored and yield an amount of `tokenOut`
/// higher than what would be obtained through a mint and burn directly on the `transmuter`
/// @param amountIn Amount of `tokenIn` to bring for the rebalancing
/// @param amountOutMin Minimum amount of `tokenOut` that must be obtained from the swap
/// @param to Address to which `tokenOut` must be sent
/// @param deadline Timestamp before which this transaction must be included
/// @return amountOut Amount of outToken obtained
function swapExactInput(
uint256 amountIn,
uint256 amountOutMin,
address tokenIn,
address tokenOut,
address to,
uint256 deadline
) external returns (uint256 amountOut);

/// @notice Approximates how much a call to `swapExactInput` with the same parameters would yield in terms
/// of `amountOut`
function quoteIn(uint256 amountIn, address tokenIn, address tokenOut) external view returns (uint256 amountOut);

/// @notice Helper to compute the minimum guaranteed amount out that would be obtained from a swap of `amountIn`
/// of `tokenIn` to `tokenOut`
function getGuaranteedAmountOut(
address tokenIn,
address tokenOut,
uint256 amountIn
) external view returns (uint256);

/// @notice Lets governance set an order to subsidize rebalances between `tokenIn` and `tokenOut`
function setOrder(address tokenIn, address tokenOut, uint256 subsidyBudget, uint256 guaranteedRate) external;

/// @notice Recovers `amount` of `token` to the `to` address
/// @dev This function checks if too much is not being recovered with respect to currently available budgets
function recover(address token, uint256 amount, address to) external;
}
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
"compile": "forge build",
"compile:dev": "FOUNDRY_PROFILE=dev forge build",
"deploy": "forge script --skip test --broadcast --verify --slow -vvvv --rpc-url polygonzkevm scripts/DeploySavings.s.sol",
"deploy:fork": "source .env && forge script --skip test --slow --fork-url fork --broadcast scripts/DeploySavings.s.sol -vvvv",
"deploy:fork": "source .env && forge script --skip test --slow --fork-url fork --broadcast scripts/DeployRebalancer.s.sol -vvvv",
"generate": "FOUNDRY_PROFILE=dev forge script scripts/utils/GenerateSelectors.s.sol",
"deploy:check": "FOUNDRY_PROFILE=dev forge script --fork-url fork scripts/test/CheckTransmuter.s.sol",
"gas": "yarn test --gas-report",
Expand Down
1 change: 1 addition & 0 deletions scripts/Constants.s.sol
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ address constant PROXY_ADMIN_GUARDIAN = 0xD9F1A8e00b0EEbeDddd9aFEaB55019D55fcec0
address constant TREASURY_EUR = 0x8667DBEBf68B0BFa6Db54f550f41Be16c4067d60;
address constant IMMUTABLE_CREATE2_FACTORY_ADDRESS = 0x0000000000FFe8B47B3e2130213B802212439497;
address constant DEPLOYER = 0xfdA462548Ce04282f4B6D6619823a7C64Fdc0185;
address constant TRANSMUTER = 0x00253582b2a3FE112feEC532221d9708c64cEFAb;

address constant EUROC = 0x1aBaEA1f7C830bD89Acc67eC4af516284b1bC33c;
address constant EUROE = 0x820802Fa8a99901F52e39acD21177b0BE6EE2974;
Expand Down
25 changes: 25 additions & 0 deletions scripts/DeployRebalancer.s.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.19;

import { Utils } from "./utils/Utils.s.sol";
import { console } from "forge-std/console.sol";
import { Rebalancer } from "contracts/helpers/Rebalancer.sol";
import { IAccessControlManager } from "contracts/utils/AccessControl.sol";
import { ITransmuter } from "contracts/interfaces/ITransmuter.sol";
import "./Constants.s.sol";
import "oz/interfaces/IERC20.sol";
import "oz-upgradeable/token/ERC20/extensions/IERC20MetadataUpgradeable.sol";

contract DeployRebalancer is Utils {
function run() external {
uint256 deployerPrivateKey = vm.deriveKey(vm.envString("MNEMONIC_FORK"), "m/44'/60'/0'/0/", 0);
vm.startBroadcast(deployerPrivateKey);

address deployer = vm.addr(deployerPrivateKey);
console.log("Deployer address: ", deployer);
Rebalancer rebalancer = new Rebalancer(IAccessControlManager(ACCESS_CONTROL_MANAGER), ITransmuter(TRANSMUTER));
console.log("Rebalancer deployed at: ", address(rebalancer));

vm.stopBroadcast();
}
}
Loading

0 comments on commit bae123f

Please sign in to comment.