Skip to content

Commit

Permalink
Upgrade Buy&Make collections (#163)
Browse files Browse the repository at this point in the history
* Upgraded recipient to support BAL tokens and batched deposits
* Add tests for gov fee collection
* Add fork tests for recipient upgrade sequence
  • Loading branch information
alsco77 committed Apr 8, 2021
1 parent d96c1a0 commit e17d633
Show file tree
Hide file tree
Showing 51 changed files with 1,967 additions and 653 deletions.
8 changes: 7 additions & 1 deletion .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,13 @@ module.exports = {
"no-console": "off",
"import/prefer-default-export": "off",
"no-nested-ternary": 1,
"@typescript-eslint/dot-notation": 1
"@typescript-eslint/dot-notation": 1,
"no-await-in-loop": 1,
"no-restricted-syntax": 1,
"@typescript-eslint/no-loop-func": 1,
"@typescript-eslint/no-unused-expressions": 1,
"lines-between-class-members": 0,
"prefer-destructuring": 1,
},
"overrides": [
{
Expand Down
28 changes: 28 additions & 0 deletions contracts/buy-and-make/Collector.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
pragma solidity 0.8.2;

import { ISavingsManager } from "../interfaces/ISavingsManager.sol";
import { ImmutableModule } from "../shared/ImmutableModule.sol";

/**
* @title Collector
* @dev Distributes unallocated interest across multiple mAssets via savingsManager
*/
contract Collector is ImmutableModule {
constructor(address _nexus) ImmutableModule(_nexus) {}

/**
* @dev Distributes the interest accrued across multiple mAssets, optionally
* calling collectAndDistribute beforehand.
*/
function distributeInterest(address[] calldata _mAssets, bool _collectFirst) external {
ISavingsManager savingsManager = ISavingsManager(_savingsManager());
uint256 len = _mAssets.length;
require(len > 0, "Invalid inputs");
for (uint256 i = 0; i < len; i++) {
if (_collectFirst) savingsManager.collectAndDistributeInterest(_mAssets[i]);

savingsManager.distributeUnallocatedInterest(_mAssets[i]);
}
}
}
18 changes: 18 additions & 0 deletions contracts/buy-and-make/IBPool.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
pragma solidity 0.8.2;

interface IBPool {
function joinswapExternAmountIn(
address tokenIn,
uint256 tokenAmountIn,
uint256 minPoolAmountOut
) external returns (uint256 poolAmountOut);

function swapExactAmountIn(
address tokenIn,
uint256 tokenAmountIn,
address tokenOut,
uint256 minAmountOut,
uint256 maxPrice
) external returns (uint256 tokenAmountOut, uint256 spotPriceAfter);
}
10 changes: 0 additions & 10 deletions contracts/buy-and-make/IConfigurableRightsPool.sol

This file was deleted.

116 changes: 92 additions & 24 deletions contracts/buy-and-make/RevenueRecipient.sol
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
pragma solidity 0.8.2;

import { IRevenueRecipient } from "../interfaces/IRevenueRecipient.sol";
import { IConfigurableRightsPool } from "./IConfigurableRightsPool.sol";
import { IBPool } from "./IBPool.sol";
import { ImmutableModule } from "../shared/ImmutableModule.sol";
import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
Expand All @@ -11,17 +11,19 @@ import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.s
* @title RevenueRecipient
* @author mStable
* @notice Simply receives mAssets and then deposits to a pre-defined Balancer
* ConfigurableRightsPool.
* @dev VERSION: 1.0
* DATE: 2021-03-08
* Bpool.
* @dev VERSION: 2.0
* DATE: 2021-04-06
*/
contract RevenueRecipient is IRevenueRecipient, ImmutableModule {
using SafeERC20 for IERC20;

event RevenueReceived(address indexed mAsset, uint256 amountIn, uint256 amountOut);
event RevenueReceived(address indexed mAsset, uint256 amountIn);
event RevenueDeposited(address indexed mAsset, uint256 amountIn, uint256 amountOut);

// BPT To which all revenue should be deposited
IConfigurableRightsPool public immutable mBPT;
IBPool public immutable mBPT;
IERC20 public immutable BAL;

// Minimum output units per 1e18 input units
mapping(address => uint256) public minOut;
Expand All @@ -30,17 +32,19 @@ contract RevenueRecipient is IRevenueRecipient, ImmutableModule {
* @dev Creates the RevenueRecipient contract
* @param _nexus mStable system Nexus address
* @param _targetPool Balancer pool to which all revenue should be deposited
* @param _balToken Address of $BAL
* @param _assets Initial list of supported mAssets
* @param _minOut Minimum BPT out per mAsset unit
*/
constructor(
address _nexus,
address _targetPool,
address _balToken,
address[] memory _assets,
uint256[] memory _minOut
) ImmutableModule(_nexus) {
mBPT = IConfigurableRightsPool(_targetPool);

mBPT = IBPool(_targetPool);
BAL = IERC20(_balToken);
uint256 len = _assets.length;
for (uint256 i = 0; i < len; i++) {
minOut[_assets[i]] = _minOut[i];
Expand All @@ -49,45 +53,109 @@ contract RevenueRecipient is IRevenueRecipient, ImmutableModule {
}

/**
* @dev Called by SavingsManager after revenue has accrued
* @dev Simply transfers the mAsset from the sender to here
* @param _mAsset Address of mAsset
* @param _amount Units of mAsset collected
*/
function notifyRedistributionAmount(address _mAsset, uint256 _amount) external override {
// Transfer from sender to here
IERC20(_mAsset).safeTransferFrom(msg.sender, address(this), _amount);

// Deposit into pool
uint256 minBPT = (_amount * minOut[_mAsset]) / 1e18;
uint256 poolAmountOut = mBPT.joinswapExternAmountIn(_mAsset, _amount, minBPT);
emit RevenueReceived(_mAsset, _amount);
}

/**
* @dev Called by anyone to deposit to the balancer pool
* @param _mAssets Addresses of assets to deposit
* @param _percentages 1e18 scaled percentages of the current balance to deposit
*/
function depositToPool(address[] calldata _mAssets, uint256[] calldata _percentages)
external
override
{
uint256 len = _mAssets.length;
require(len > 0 && len == _percentages.length, "Invalid args");

for (uint256 i = 0; i < len; i++) {
uint256 pct = _percentages[i];
require(pct > 1e15 && pct <= 1e18, "Invalid pct");
address mAsset = _mAssets[i];
uint256 bal = IERC20(mAsset).balanceOf(address(this));
// e.g. 1 * 5e17 / 1e18 = 5e17
uint256 deposit = (bal * pct) / 1e18;
require(minOut[mAsset] > 0, "Invalid minout");
uint256 minBPT = (deposit * minOut[mAsset]) / 1e18;
uint256 poolAmountOut = mBPT.joinswapExternAmountIn(mAsset, deposit, minBPT);

emit RevenueReceived(_mAsset, _amount, poolAmountOut);
emit RevenueDeposited(mAsset, deposit, poolAmountOut);
}
}

/**
* @dev Simply approves spending of a given mAsset by BPT
* @param _mAsset Address of mAsset to approve
* @dev Simply approves spending of a given asset by BPT
* @param asset Address of asset to approve
*/
function approveAsset(address _mAsset) external onlyGovernor {
IERC20(_mAsset).safeApprove(address(mBPT), 0);
IERC20(_mAsset).safeApprove(address(mBPT), 2**256 - 1);
function approveAsset(address asset) external onlyGovernor {
IERC20(asset).safeApprove(address(mBPT), 0);
IERC20(asset).safeApprove(address(mBPT), 2**256 - 1);
}

/**
* @dev Sets the minimum amount of BPT to receive for a given mAsset
* @param _mAsset Address of mAsset
* @dev Sets the minimum amount of BPT to receive for a given asset
* @param _asset Address of mAsset
* @param _minOut Scaled amount to receive per 1e18 mAsset units
*/
function updateAmountOut(address _mAsset, uint256 _minOut) external onlyGovernor {
minOut[_mAsset] = _minOut;
function updateAmountOut(address _asset, uint256 _minOut) external onlyGovernor {
minOut[_asset] = _minOut;
}

/**
* @dev Migrates BPT to a new revenue recipient
* @dev Migrates BPT and BAL to a new revenue recipient
* @param _recipient Address of recipient
*/
function migrateBPT(address _recipient) external onlyGovernor {
function migrate(address _recipient) external onlyGovernor {
IERC20 mBPT_ = IERC20(address(mBPT));
mBPT_.safeTransfer(_recipient, mBPT_.balanceOf(address(this)));
BAL.safeTransfer(_recipient, BAL.balanceOf(address(this)));
}

/**
* @dev Reinvests any accrued $BAL tokens back into the pool
* @param _pool Address of the bPool to swap into
* @param _output Token to receive out of the swap (must be in mBPT)
* @param _minAmountOut TOTAL amount out for the $BAL -> _output swap
* @param _maxPrice MaxPrice for the output (req by bPool)
* @param _pct Percentage of all BAL held here to liquidate
*/
function reinvestBAL(
address _pool,
address _output,
uint256 _minAmountOut,
uint256 _maxPrice,
uint256 _pct
) external onlyGovernor {
require(minOut[_output] > 0, "Invalid output");
require(_pct > 1e15 && _pct <= 1e18, "Invalid pct");
uint256 balance = BAL.balanceOf(address(this));
uint256 balDeposit = (balance * _pct) / 1e18;
// 1. Convert BAL to ETH
BAL.approve(_pool, balDeposit);
(uint256 tokenAmountOut, ) =
IBPool(_pool).swapExactAmountIn(
address(BAL),
balDeposit,
_output,
_minAmountOut,
_maxPrice
);
// 2. Deposit ETH to mBPT
uint256 poolAmountOut =
mBPT.joinswapExternAmountIn(
_output,
tokenAmountOut,
(tokenAmountOut * minOut[_output]) / 1e18
);

emit RevenueDeposited(_output, tokenAmountOut, poolAmountOut);
}
}
4 changes: 2 additions & 2 deletions contracts/feeders/FeederPool.sol
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ contract FeederPool is
// Constants
uint256 private constant MAX_FEE = 1e16;
uint256 private constant A_PRECISION = 100;
address public immutable mAsset;
address public immutable override mAsset;

// Core data storage
FeederData public data;
Expand Down Expand Up @@ -783,7 +783,7 @@ contract FeederPool is
/**
* @dev Collects the pending gov fees extracted from swap, redeem and platform interest.
*/
function collectPendingFees() external onlyInterestValidator {
function collectPendingFees() external override onlyInterestValidator {
uint256 fees = data.pendingFees;
if (fees > 1) {
uint256 mintAmount = fees - 1;
Expand Down
35 changes: 31 additions & 4 deletions contracts/feeders/InterestValidator.sol
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ pragma solidity 0.8.2;

import { IFeederPool } from "../interfaces/IFeederPool.sol";
import { PausableModule } from "../shared/PausableModule.sol";
import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";

/**
* @title InterestValidator
Expand All @@ -21,6 +23,7 @@ contract InterestValidator is PausableModule {
uint256 newTotalSupply,
uint256 apy
);
event GovFeeCollected(address indexed feederPool, address mAsset, uint256 amount);

// Utils to help keep interest under check
uint256 private constant SECONDS_IN_YEAR = 365 days;
Expand All @@ -35,15 +38,15 @@ contract InterestValidator is PausableModule {
* @notice Collects and validates the interest of n feeder pools.
* @dev First calls to calculate the interest that has accrued, and then validates the potential inflation
* with respect to the previous timestamp.
* @param _feeders Addresses of the feeder pools on which to accrue interest
* @param _fPools Addresses of the feeder pools on which to accrue interest
*/
function collectAndValidateInterest(address[] calldata _feeders) external whenNotPaused {
function collectAndValidateInterest(address[] calldata _fPools) external whenNotPaused {
uint256 currentTime = block.timestamp;

uint256 len = _feeders.length;
uint256 len = _fPools.length;

for (uint256 i = 0; i < len; i++) {
address feeder = _feeders[i];
address feeder = _fPools[i];

uint256 previousBatch = lastBatchCollected[feeder];
uint256 timeSincePreviousBatch = currentTime - previousBatch;
Expand All @@ -66,6 +69,30 @@ contract InterestValidator is PausableModule {
}
}

/**
* @dev Collects gov fees from fPools in the form of fPtoken, then converts to
* mAsset and sends directly to the SavingsManager, where it will be picked up and
* converted to mBPT upon the next collection
*/
function collectGovFees(address[] calldata _fPools) external onlyGovernor {
uint256 len = _fPools.length;

address savingsManager = _savingsManager();
for (uint256 i = 0; i < len; i++) {
address fPool = _fPools[i];
// 1. Collect pending fees
IFeederPool(fPool).collectPendingFees();
uint256 fpTokenBal = IERC20(fPool).balanceOf(address(this));
// 2. If fpTokenBal > 0, convert to mAsset and transfer to savingsManager
if (fpTokenBal > 0) {
address mAsset = IFeederPool(fPool).mAsset();
uint256 outputAmt =
IFeederPool(fPool).redeem(mAsset, fpTokenBal, (fpTokenBal * 7) / 10, savingsManager);
emit GovFeeCollected(fPool, mAsset, outputAmt);
}
}
}

/**
* @dev Validates that an interest collection does not exceed a maximum APY. If last collection
* was under 30 mins ago, simply check it does not exceed 10bps
Expand Down
2 changes: 1 addition & 1 deletion contracts/interfaces/IBoostDirector.sol
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,4 @@ interface IBoostDirector {
) external;

function whitelistVaults(address[] calldata _vaults) external;
}
}
10 changes: 7 additions & 3 deletions contracts/interfaces/IFeederPool.sol
Original file line number Diff line number Diff line change
Expand Up @@ -50,13 +50,13 @@ abstract contract IFeederPool {
// Redemption
function redeem(
address _output,
uint256 _mAssetQuantity,
uint256 _fpTokenQuantity,
uint256 _minOutputQuantity,
address _recipient
) external virtual returns (uint256 outputQuantity);

function redeemProportionately(
uint256 _mAssetQuantity,
uint256 _fpTokenQuantity,
uint256[] calldata _minOutputQuantities,
address _recipient
) external virtual returns (uint256[] memory outputQuantities);
Expand All @@ -68,7 +68,7 @@ abstract contract IFeederPool {
address _recipient
) external virtual returns (uint256 mAssetRedeemed);

function getRedeemOutput(address _output, uint256 _mAssetQuantity)
function getRedeemOutput(address _output, uint256 _fpTokenQuantity)
external
view
virtual
Expand All @@ -80,6 +80,8 @@ abstract contract IFeederPool {
) external view virtual returns (uint256 mAssetAmount);

// Views
function mAsset() external view virtual returns (address);

function getPrice() public view virtual returns (uint256 price, uint256 k);

function getConfig() external view virtual returns (FeederConfig memory config);
Expand All @@ -101,4 +103,6 @@ abstract contract IFeederPool {
external
virtual
returns (uint256 mintAmount, uint256 newSupply);

function collectPendingFees() external virtual;
}
2 changes: 2 additions & 0 deletions contracts/interfaces/IRevenueRecipient.sol
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,6 @@ pragma solidity 0.8.2;
interface IRevenueRecipient {
/** @dev Recipient */
function notifyRedistributionAmount(address _mAsset, uint256 _amount) external;

function depositToPool(address[] calldata _mAssets, uint256[] calldata _percentages) external;
}
Loading

0 comments on commit e17d633

Please sign in to comment.