// SPDX-License-Identifier: AGPL-3.0-or-later
pragma solidity 0.8.6;
struct BassetPersonal {
// Address of the bAsset
address addr;
// Address of the bAsset
address integrator;
// An ERC20 can charge transfer fee, for example USDT, DGX tokens.
bool hasTxFee; // takes a byte in storage
// Status of the bAsset
BassetStatus status;
}
struct BassetData {
// 1 Basset * ratio / ratioScale == x Masset (relative value)
// If ratio == 10e8 then 1 bAsset = 10 mAssets
// A ratio is divised as 10^(18-tokenDecimals) * measurementMultiple(relative value of 1 base unit)
uint128 ratio;
// Amount of the Basset that is held in Collateral
uint128 vaultBalance;
}
// Status of the Basset - has it broken its peg?
enum BassetStatus {
Default,
Normal,
BrokenBelowPeg,
BrokenAbovePeg,
Blacklisted,
Liquidating,
Liquidated,
Fail
}
abstract contract IMasset {
// Mint
function mint(
address _input,
uint256 _inputQuantity,
uint256 _minOutputQuantity,
address _recipient
) external virtual returns (uint256 mintOutput);
function mintMulti(
address[] calldata _inputs,
uint256[] calldata _inputQuantities,
uint256 _minOutputQuantity,
address _recipient
) external virtual returns (uint256 mintOutput);
function getMintOutput(address _input, uint256 _inputQuantity)
external
view
virtual
returns (uint256 mintOutput);
function getMintMultiOutput(address[] calldata _inputs, uint256[] calldata _inputQuantities)
external
view
virtual
returns (uint256 mintOutput);
// Swaps
function swap(
address _input,
address _output,
uint256 _inputQuantity,
uint256 _minOutputQuantity,
address _recipient
) external virtual returns (uint256 swapOutput);
function getSwapOutput(
address _input,
address _output,
uint256 _inputQuantity
) external view virtual returns (uint256 swapOutput);
// Redemption
function redeem(
address _output,
uint256 _mAssetQuantity,
uint256 _minOutputQuantity,
address _recipient
) external virtual returns (uint256 outputQuantity);
function redeemMasset(
uint256 _mAssetQuantity,
uint256[] calldata _minOutputQuantities,
address _recipient
) external virtual returns (uint256[] memory outputQuantities);
function redeemExactBassets(
address[] calldata _outputs,
uint256[] calldata _outputQuantities,
uint256 _maxMassetQuantity,
address _recipient
) external virtual returns (uint256 mAssetRedeemed);
function getRedeemOutput(address _output, uint256 _mAssetQuantity)
external
view
virtual
returns (uint256 bAssetOutput);
function getRedeemExactBassetsOutput(
address[] calldata _outputs,
uint256[] calldata _outputQuantities
) external view virtual returns (uint256 mAssetAmount);
// Views
function getBasket() external view virtual returns (bool, bool);
function getBasset(address _token)
external
view
virtual
returns (BassetPersonal memory personal, BassetData memory data);
function getBassets()
external
view
virtual
returns (BassetPersonal[] memory personal, BassetData[] memory data);
function bAssetIndexes(address) external view virtual returns (uint8);
function getPrice() external view virtual returns (uint256 price, uint256 k);
// SavingsManager
function collectInterest() external virtual returns (uint256 swapFeesGained, uint256 newSupply);
function collectPlatformInterest()
external
virtual
returns (uint256 mintAmount, uint256 newSupply);
// Admin
function setCacheSize(uint256 _cacheSize) external virtual;
function setFees(uint256 _swapFee, uint256 _redemptionFee) external virtual;
function setTransferFeesFlag(address _bAsset, bool _flag) external virtual;
function migrateBassets(address[] calldata _bAssets, address _newIntegration) external virtual;
}
interface IERC20 {
/**
* @dev Returns the amount of tokens in existence.
*/
function totalSupply() external view returns (uint256);
/**
* @dev Returns the amount of tokens owned by `account`.
*/
function balanceOf(address account) external view returns (uint256);
/**
* @dev Moves `amount` tokens from the caller's account to `recipient`.
*
* Returns a boolean value indicating whether the operation succeeded.
*
* Emits a {Transfer} event.
*/
function transfer(address recipient, uint256 amount) external returns (bool);
/**
* @dev Returns the remaining number of tokens that `spender` will be
* allowed to spend on behalf of `owner` through {transferFrom}. This is
* zero by default.
*
* This value changes when {approve} or {transferFrom} are called.
*/
function allowance(address owner, address spender) external view returns (uint256);
/**
* @dev Sets `amount` as the allowance of `spender` over the caller's tokens.
*
* Returns a boolean value indicating whether the operation succeeded.
*
* IMPORTANT: Beware that changing an allowance with this method brings the risk
* that someone may use both the old and the new allowance by unfortunate
* transaction ordering. One possible solution to mitigate this race
* condition is to first reduce the spender's allowance to 0 and set the
* desired value afterwards:
* https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729
*
* Emits an {Approval} event.
*/
function approve(address spender, uint256 amount) external returns (bool);
/**
* @dev Moves `amount` tokens from `sender` to `recipient` using the
* allowance mechanism. `amount` is then deducted from the caller's
* allowance.
*
* Returns a boolean value indicating whether the operation succeeded.
*
* Emits a {Transfer} event.
*/
function transferFrom(
address sender,
address recipient,
uint256 amount
) external returns (bool);
/**
* @dev Emitted when `value` tokens are moved from one account (`from`) to
* another (`to`).
*
* Note that `value` may be zero.
*/
event Transfer(address indexed from, address indexed to, uint256 value);
/**
* @dev Emitted when the allowance of a `spender` for an `owner` is set by
* a call to {approve}. `value` is the new allowance.
*/
event Approval(address indexed owner, address indexed spender, uint256 value);
}
interface ISavingsContractV2 {
// DEPRECATED but still backwards compatible
function redeem(uint256 _amount) external returns (uint256 massetReturned);
function creditBalances(address) external view returns (uint256); // V1 & V2 (use balanceOf)
// --------------------------------------------
function depositInterest(uint256 _amount) external; // V1 & V2
function depositSavings(uint256 _amount) external returns (uint256 creditsIssued); // V1 & V2
function depositSavings(uint256 _amount, address _beneficiary)
external
returns (uint256 creditsIssued); // V2
function redeemCredits(uint256 _amount) external returns (uint256 underlyingReturned); // V2
function redeemUnderlying(uint256 _amount) external returns (uint256 creditsBurned); // V2
function exchangeRate() external view returns (uint256); // V1 & V2
function balanceOfUnderlying(address _user) external view returns (uint256 underlying); // V2
function underlyingToCredits(uint256 _underlying) external view returns (uint256 credits); // V2
function creditsToUnderlying(uint256 _credits) external view returns (uint256 underlying); // V2
function underlying() external view returns (IERC20 underlyingMasset); // V2
}
interface IRevenueRecipient {
/** @dev Recipient */
function notifyRedistributionAmount(address _mAsset, uint256 _amount) external;
function depositToPool(address[] calldata _mAssets, uint256[] calldata _percentages) external;
}
interface ISavingsManager {
/** @dev Admin privs */
function distributeUnallocatedInterest(address _mAsset) external;
/** @dev Liquidator */
function depositLiquidation(address _mAsset, uint256 _liquidation) external;
/** @dev Liquidator */
function collectAndStreamInterest(address _mAsset) external;
/** @dev Public privs */
function collectAndDistributeInterest(address _mAsset) external;
/** @dev getter for public lastBatchCollected mapping */
function lastBatchCollected(address _mAsset) external view returns (uint256);
}
contract ModuleKeys {
// Governance
// ===========
// keccak256("Governance");
bytes32 internal constant KEY_GOVERNANCE =
0x9409903de1e6fd852dfc61c9dacb48196c48535b60e25abf92acc92dd689078d;
//keccak256("Staking");
bytes32 internal constant KEY_STAKING =
0x1df41cd916959d1163dc8f0671a666ea8a3e434c13e40faef527133b5d167034;
//keccak256("ProxyAdmin");
bytes32 internal constant KEY_PROXY_ADMIN =
0x96ed0203eb7e975a4cbcaa23951943fa35c5d8288117d50c12b3d48b0fab48d1;
// mStable
// =======
// keccak256("OracleHub");
bytes32 internal constant KEY_ORACLE_HUB =
0x8ae3a082c61a7379e2280f3356a5131507d9829d222d853bfa7c9fe1200dd040;
// keccak256("Manager");
bytes32 internal constant KEY_MANAGER =
0x6d439300980e333f0256d64be2c9f67e86f4493ce25f82498d6db7f4be3d9e6f;
//keccak256("Recollateraliser");
bytes32 internal constant KEY_RECOLLATERALISER =
0x39e3ed1fc335ce346a8cbe3e64dd525cf22b37f1e2104a755e761c3c1eb4734f;
//keccak256("MetaToken");
bytes32 internal constant KEY_META_TOKEN =
0xea7469b14936af748ee93c53b2fe510b9928edbdccac3963321efca7eb1a57a2;
// keccak256("SavingsManager");
bytes32 internal constant KEY_SAVINGS_MANAGER =
0x12fe936c77a1e196473c4314f3bed8eeac1d757b319abb85bdda70df35511bf1;
// keccak256("Liquidator");
bytes32 internal constant KEY_LIQUIDATOR =
0x1e9cb14d7560734a61fa5ff9273953e971ff3cd9283c03d8346e3264617933d4;
// keccak256("InterestValidator");
bytes32 internal constant KEY_INTEREST_VALIDATOR =
0xc10a28f028c7f7282a03c90608e38a4a646e136e614e4b07d119280c5f7f839f;
}
interface INexus {
function governor() external view returns (address);
function getModule(bytes32 key) external view returns (address);
function proposeModule(bytes32 _key, address _addr) external;
function cancelProposedModule(bytes32 _key) external;
function acceptProposedModule(bytes32 _key) external;
function acceptProposedModules(bytes32[] calldata _keys) external;
function requestLockModule(bytes32 _key) external;
function cancelLockModule(bytes32 _key) external;
function lockModule(bytes32 _key) external;
}
abstract contract ImmutableModule is ModuleKeys {
INexus public immutable nexus;
/**
* @dev Initialization function for upgradable proxy contracts
* @param _nexus Nexus contract address
*/
constructor(address _nexus) {
require(_nexus != address(0), "Nexus address is zero");
nexus = INexus(_nexus);
}
/**
* @dev Modifier to allow function calls only from the Governor.
*/
modifier onlyGovernor() {
_onlyGovernor();
_;
}
function _onlyGovernor() internal view {
require(msg.sender == _governor(), "Only governor can execute");
}
/**
* @dev Modifier to allow function calls only from the Governance.
* Governance is either Governor address or Governance address.
*/
modifier onlyGovernance() {
require(
msg.sender == _governor() || msg.sender == _governance(),
"Only governance can execute"
);
_;
}
/**
* @dev Returns Governor address from the Nexus
* @return Address of Governor Contract
*/
function _governor() internal view returns (address) {
return nexus.governor();
}
/**
* @dev Returns Governance Module address from the Nexus
* @return Address of the Governance (Phase 2)
*/
function _governance() internal view returns (address) {
return nexus.getModule(KEY_GOVERNANCE);
}
/**
* @dev Return SavingsManager Module address from the Nexus
* @return Address of the SavingsManager Module contract
*/
function _savingsManager() internal view returns (address) {
return nexus.getModule(KEY_SAVINGS_MANAGER);
}
/**
* @dev Return Recollateraliser Module address from the Nexus
* @return Address of the Recollateraliser Module contract (Phase 2)
*/
function _recollateraliser() internal view returns (address) {
return nexus.getModule(KEY_RECOLLATERALISER);
}
/**
* @dev Return Liquidator Module address from the Nexus
* @return Address of the Liquidator Module contract
*/
function _liquidator() internal view returns (address) {
return nexus.getModule(KEY_LIQUIDATOR);
}
/**
* @dev Return ProxyAdmin Module address from the Nexus
* @return Address of the ProxyAdmin Module contract
*/
function _proxyAdmin() internal view returns (address) {
return nexus.getModule(KEY_PROXY_ADMIN);
}
}
abstract contract PausableModule is ImmutableModule {
/**
* @dev Emitted when the pause is triggered by Governor
*/
event Paused(address account);
/**
* @dev Emitted when the pause is lifted by Governor
*/
event Unpaused(address account);
bool internal _paused = false;
/**
* @dev Modifier to make a function callable only when the contract is not paused.
*/
modifier whenNotPaused() {
require(!_paused, "Pausable: paused");
_;
}
/**
* @dev Modifier to make a function callable only when the contract is paused.
*/
modifier whenPaused() {
require(_paused, "Pausable: not paused");
_;
}
/**
* @dev Initializes the contract in unpaused state.
* Hooks into the Module to give the Governor ability to pause
* @param _nexus Nexus contract address
*/
constructor(address _nexus) ImmutableModule(_nexus) {
_paused = false;
}
/**
* @dev Returns true if the contract is paused, and false otherwise.
* @return Returns `true` when paused, otherwise `false`
*/
function paused() external view returns (bool) {
return _paused;
}
/**
* @dev Called by the Governor to pause, triggers stopped state.
*/
function pause() external onlyGovernor whenNotPaused {
_paused = true;
emit Paused(msg.sender);
}
/**
* @dev Called by Governor to unpause, returns to normal state.
*/
function unpause() external onlyGovernor whenPaused {
_paused = false;
emit Unpaused(msg.sender);
}
}
/**
* @dev Collection of functions related to the address type
*/
library Address {
/**
* @dev Returns true if `account` is a contract.
*
* [IMPORTANT]
* ====
* It is unsafe to assume that an address for which this function returns
* false is an externally-owned account (EOA) and not a contract.
*
* Among others, `isContract` will return false for the following
* types of addresses:
*
* - an externally-owned account
* - a contract in construction
* - an address where a contract will be created
* - an address where a contract lived, but was destroyed
* ====
*/
function isContract(address account) internal view returns (bool) {
// This method relies on extcodesize, which returns 0 for contracts in
// construction, since the code is only stored at the end of the
// constructor execution.
uint256 size;
assembly {
size := extcodesize(account)
}
return size > 0;
}
/**
* @dev Replacement for Solidity's `transfer`: sends `amount` wei to
* `recipient`, forwarding all available gas and reverting on errors.
*
* https://eips.ethereum.org/EIPS/eip-1884[EIP1884] increases the gas cost
* of certain opcodes, possibly making contracts go over the 2300 gas limit
* imposed by `transfer`, making them unable to receive funds via
* `transfer`. {sendValue} removes this limitation.
*
* https://diligence.consensys.net/posts/2019/09/stop-using-soliditys-transfer-now/[Learn more].
*
* IMPORTANT: because control is transferred to `recipient`, care must be
* taken to not create reentrancy vulnerabilities. Consider using
* {ReentrancyGuard} or the
* https://solidity.readthedocs.io/en/v0.5.11/security-considerations.html#use-the-checks-effects-interactions-pattern[checks-effects-interactions pattern].
*/
function sendValue(address payable recipient, uint256 amount) internal {
require(address(this).balance >= amount, "Address: insufficient balance");
(bool success, ) = recipient.call{value: amount}("");
require(success, "Address: unable to send value, recipient may have reverted");
}
/**
* @dev Performs a Solidity function call using a low level `call`. A
* plain `call` is an unsafe replacement for a function call: use this
* function instead.
*
* If `target` reverts with a revert reason, it is bubbled up by this
* function (like regular Solidity function calls).
*
* Returns the raw returned data. To convert to the expected return value,
* use https://solidity.readthedocs.io/en/latest/units-and-global-variables.html?highlight=abi.decode#abi-encoding-and-decoding-functions[`abi.decode`].
*
* Requirements:
*
* - `target` must be a contract.
* - calling `target` with `data` must not revert.
*
* _Available since v3.1._
*/
function functionCall(address target, bytes memory data) internal returns (bytes memory) {
return functionCall(target, data, "Address: low-level call failed");
}
/**
* @dev Same as {xref-Address-functionCall-address-bytes-}[`functionCall`], but with
* `errorMessage` as a fallback revert reason when `target` reverts.
*
* _Available since v3.1._
*/
function functionCall(
address target,
bytes memory data,
string memory errorMessage
) internal returns (bytes memory) {
return functionCallWithValue(target, data, 0, errorMessage);
}
/**
* @dev Same as {xref-Address-functionCall-address-bytes-}[`functionCall`],
* but also transferring `value` wei to `target`.
*
* Requirements:
*
* - the calling contract must have an ETH balance of at least `value`.
* - the called Solidity function must be `payable`.
*
* _Available since v3.1._
*/
function functionCallWithValue(
address target,
bytes memory data,
uint256 value
) internal returns (bytes memory) {
return functionCallWithValue(target, data, value, "Address: low-level call with value failed");
}
/**
* @dev Same as {xref-Address-functionCallWithValue-address-bytes-uint256-}[`functionCallWithValue`], but
* with `errorMessage` as a fallback revert reason when `target` reverts.
*
* _Available since v3.1._
*/
function functionCallWithValue(
address target,
bytes memory data,
uint256 value,
string memory errorMessage
) internal returns (bytes memory) {
require(address(this).balance >= value, "Address: insufficient balance for call");
require(isContract(target), "Address: call to non-contract");
(bool success, bytes memory returndata) = target.call{value: value}(data);
return _verifyCallResult(success, returndata, errorMessage);
}
/**
* @dev Same as {xref-Address-functionCall-address-bytes-}[`functionCall`],
* but performing a static call.
*
* _Available since v3.3._
*/
function functionStaticCall(address target, bytes memory data) internal view returns (bytes memory) {
return functionStaticCall(target, data, "Address: low-level static call failed");
}
/**
* @dev Same as {xref-Address-functionCall-address-bytes-string-}[`functionCall`],
* but performing a static call.
*
* _Available since v3.3._
*/
function functionStaticCall(
address target,
bytes memory data,
string memory errorMessage
) internal view returns (bytes memory) {
require(isContract(target), "Address: static call to non-contract");
(bool success, bytes memory returndata) = target.staticcall(data);
return _verifyCallResult(success, returndata, errorMessage);
}
/**
* @dev Same as {xref-Address-functionCall-address-bytes-}[`functionCall`],
* but performing a delegate call.
*
* _Available since v3.4._
*/
function functionDelegateCall(address target, bytes memory data) internal returns (bytes memory) {
return functionDelegateCall(target, data, "Address: low-level delegate call failed");
}
/**
* @dev Same as {xref-Address-functionCall-address-bytes-string-}[`functionCall`],
* but performing a delegate call.
*
* _Available since v3.4._
*/
function functionDelegateCall(
address target,
bytes memory data,
string memory errorMessage
) internal returns (bytes memory) {
require(isContract(target), "Address: delegate call to non-contract");
(bool success, bytes memory returndata) = target.delegatecall(data);
return _verifyCallResult(success, returndata, errorMessage);
}
function _verifyCallResult(
bool success,
bytes memory returndata,
string memory errorMessage
) private pure returns (bytes memory) {
if (success) {
return returndata;
} else {
// Look for revert reason and bubble it up if present
if (returndata.length > 0) {
// The easiest way to bubble the revert reason is using memory via assembly
assembly {
let returndata_size := mload(returndata)
revert(add(32, returndata), returndata_size)
}
} else {
revert(errorMessage);
}
}
}
}
library SafeERC20 {
using Address for address;
function safeTransfer(
IERC20 token,
address to,
uint256 value
) internal {
_callOptionalReturn(token, abi.encodeWithSelector(token.transfer.selector, to, value));
}
function safeTransferFrom(
IERC20 token,
address from,
address to,
uint256 value
) internal {
_callOptionalReturn(token, abi.encodeWithSelector(token.transferFrom.selector, from, to, value));
}
/**
* @dev Deprecated. This function has issues similar to the ones found in
* {IERC20-approve}, and its usage is discouraged.
*
* Whenever possible, use {safeIncreaseAllowance} and
* {safeDecreaseAllowance} instead.
*/
function safeApprove(
IERC20 token,
address spender,
uint256 value
) internal {
// safeApprove should only be called when setting an initial allowance,
// or when resetting it to zero. To increase and decrease it, use
// 'safeIncreaseAllowance' and 'safeDecreaseAllowance'
require(
(value == 0) || (token.allowance(address(this), spender) == 0),
"SafeERC20: approve from non-zero to non-zero allowance"
);
_callOptionalReturn(token, abi.encodeWithSelector(token.approve.selector, spender, value));
}
function safeIncreaseAllowance(
IERC20 token,
address spender,
uint256 value
) internal {
uint256 newAllowance = token.allowance(address(this), spender) + value;
_callOptionalReturn(token, abi.encodeWithSelector(token.approve.selector, spender, newAllowance));
}
function safeDecreaseAllowance(
IERC20 token,
address spender,
uint256 value
) internal {
unchecked {
uint256 oldAllowance = token.allowance(address(this), spender);
require(oldAllowance >= value, "SafeERC20: decreased allowance below zero");
uint256 newAllowance = oldAllowance - value;
_callOptionalReturn(token, abi.encodeWithSelector(token.approve.selector, spender, newAllowance));
}
}
/**
* @dev Imitates a Solidity high-level call (i.e. a regular function call to a contract), relaxing the requirement
* on the return value: the return value is optional (but if data is returned, it must not be false).
* @param token The token targeted by the call.
* @param data The call data (encoded using abi.encode or one of its variants).
*/
function _callOptionalReturn(IERC20 token, bytes memory data) private {
// We need to perform a low level call here, to bypass Solidity's return data size checking mechanism, since
// we're implementing it ourselves. We use {Address.functionCall} to perform this call, which verifies that
// the target address contains contract code and also asserts for success in the low-level call.
bytes memory returndata = address(token).functionCall(data, "SafeERC20: low-level call failed");
if (returndata.length > 0) {
// Return data is optional
require(abi.decode(returndata, (bool)), "SafeERC20: ERC20 operation did not succeed");
}
}
}
library StableMath {
/**
* @dev Scaling unit for use in specific calculations,
* where 1 * 10**18, or 1e18 represents a unit '1'
*/
uint256 private constant FULL_SCALE = 1e18;
/**
* @dev Token Ratios are used when converting between units of bAsset, mAsset and MTA
* Reasoning: Takes into account token decimals, and difference in base unit (i.e. grams to Troy oz for gold)
* bAsset ratio unit for use in exact calculations,
* where (1 bAsset unit * bAsset.ratio) / ratioScale == x mAsset unit
*/
uint256 private constant RATIO_SCALE = 1e8;
/**
* @dev Provides an interface to the scaling unit
* @return Scaling unit (1e18 or 1 * 10**18)
*/
function getFullScale() internal pure returns (uint256) {
return FULL_SCALE;
}
/**
* @dev Provides an interface to the ratio unit
* @return Ratio scale unit (1e8 or 1 * 10**8)
*/
function getRatioScale() internal pure returns (uint256) {
return RATIO_SCALE;
}
/**
* @dev Scales a given integer to the power of the full scale.
* @param x Simple uint256 to scale
* @return Scaled value a to an exact number
*/
function scaleInteger(uint256 x) internal pure returns (uint256) {
return x * FULL_SCALE;
}
/***************************************
PRECISE ARITHMETIC
****************************************/
/**
* @dev Multiplies two precise units, and then truncates by the full scale
* @param x Left hand input to multiplication
* @param y Right hand input to multiplication
* @return Result after multiplying the two inputs and then dividing by the shared
* scale unit
*/
function mulTruncate(uint256 x, uint256 y) internal pure returns (uint256) {
return mulTruncateScale(x, y, FULL_SCALE);
}
/**
* @dev Multiplies two precise units, and then truncates by the given scale. For example,
* when calculating 90% of 10e18, (10e18 * 9e17) / 1e18 = (9e36) / 1e18 = 9e18
* @param x Left hand input to multiplication
* @param y Right hand input to multiplication
* @param scale Scale unit
* @return Result after multiplying the two inputs and then dividing by the shared
* scale unit
*/
function mulTruncateScale(
uint256 x,
uint256 y,
uint256 scale
) internal pure returns (uint256) {
// e.g. assume scale = fullScale
// z = 10e18 * 9e17 = 9e36
// return 9e36 / 1e18 = 9e18
return (x * y) / scale;
}
/**
* @dev Multiplies two precise units, and then truncates by the full scale, rounding up the result
* @param x Left hand input to multiplication
* @param y Right hand input to multiplication
* @return Result after multiplying the two inputs and then dividing by the shared
* scale unit, rounded up to the closest base unit.
*/
function mulTruncateCeil(uint256 x, uint256 y) internal pure returns (uint256) {
// e.g. 8e17 * 17268172638 = 138145381104e17
uint256 scaled = x * y;
// e.g. 138145381104e17 + 9.99...e17 = 138145381113.99...e17
uint256 ceil = scaled + FULL_SCALE - 1;
// e.g. 13814538111.399...e18 / 1e18 = 13814538111
return ceil / FULL_SCALE;
}
/**
* @dev Precisely divides two units, by first scaling the left hand operand. Useful
* for finding percentage weightings, i.e. 8e18/10e18 = 80% (or 8e17)
* @param x Left hand input to division
* @param y Right hand input to division
* @return Result after multiplying the left operand by the scale, and
* executing the division on the right hand input.
*/
function divPrecisely(uint256 x, uint256 y) internal pure returns (uint256) {
// e.g. 8e18 * 1e18 = 8e36
// e.g. 8e36 / 10e18 = 8e17
return (x * FULL_SCALE) / y;
}
/***************************************
RATIO FUNCS
****************************************/
/**
* @dev Multiplies and truncates a token ratio, essentially flooring the result
* i.e. How much mAsset is this bAsset worth?
* @param x Left hand operand to multiplication (i.e Exact quantity)
* @param ratio bAsset ratio
* @return c Result after multiplying the two inputs and then dividing by the ratio scale
*/
function mulRatioTruncate(uint256 x, uint256 ratio) internal pure returns (uint256 c) {
return mulTruncateScale(x, ratio, RATIO_SCALE);
}
/**
* @dev Multiplies and truncates a token ratio, rounding up the result
* i.e. How much mAsset is this bAsset worth?
* @param x Left hand input to multiplication (i.e Exact quantity)
* @param ratio bAsset ratio
* @return Result after multiplying the two inputs and then dividing by the shared
* ratio scale, rounded up to the closest base unit.
*/
function mulRatioTruncateCeil(uint256 x, uint256 ratio) internal pure returns (uint256) {
// e.g. How much mAsset should I burn for this bAsset (x)?
// 1e18 * 1e8 = 1e26
uint256 scaled = x * ratio;
// 1e26 + 9.99e7 = 100..00.999e8
uint256 ceil = scaled + RATIO_SCALE - 1;
// return 100..00.999e8 / 1e8 = 1e18
return ceil / RATIO_SCALE;
}
/**
* @dev Precisely divides two ratioed units, by first scaling the left hand operand
* i.e. How much bAsset is this mAsset worth?
* @param x Left hand operand in division
* @param ratio bAsset ratio
* @return c Result after multiplying the left operand by the scale, and
* executing the division on the right hand input.
*/
function divRatioPrecisely(uint256 x, uint256 ratio) internal pure returns (uint256 c) {
// e.g. 1e14 * 1e8 = 1e22
// return 1e22 / 1e12 = 1e10
return (x * RATIO_SCALE) / ratio;
}
/***************************************
HELPERS
****************************************/
/**
* @dev Calculates minimum of two numbers
* @param x Left hand input
* @param y Right hand input
* @return Minimum of the two inputs
*/
function min(uint256 x, uint256 y) internal pure returns (uint256) {
return x > y ? y : x;
}
/**
* @dev Calculated maximum of two numbers
* @param x Left hand input
* @param y Right hand input
* @return Maximum of the two inputs
*/
function max(uint256 x, uint256 y) internal pure returns (uint256) {
return x > y ? x : y;
}
/**
* @dev Clamps a value to an upper bound
* @param x Left hand input
* @param upperBound Maximum possible value to return
* @return Input x clamped to a maximum value, upperBound
*/
function clamp(uint256 x, uint256 upperBound) internal pure returns (uint256) {
return x > upperBound ? upperBound : x;
}
}
library YieldValidator {
uint256 private constant SECONDS_IN_YEAR = 365 days;
uint256 private constant THIRTY_MINUTES = 30 minutes;
uint256 private constant MAX_APY = 15e18;
uint256 private constant TEN_BPS = 1e15;
/**
* @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
* @param _newSupply New total supply of the mAsset
* @param _interest Increase in total supply since last collection
* @param _timeSinceLastCollection Seconds since last collection
*/
function validateCollection(
uint256 _newSupply,
uint256 _interest,
uint256 _timeSinceLastCollection
) internal pure returns (uint256 extrapolatedAPY) {
return
validateCollection(_newSupply, _interest, _timeSinceLastCollection, MAX_APY, TEN_BPS);
}
/**
* @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
* @param _newSupply New total supply of the mAsset
* @param _interest Increase in total supply since last collection
* @param _timeSinceLastCollection Seconds since last collection
* @param _maxApy Max APY where 100% == 1e18
* @param _baseApy If less than 30 mins, do not exceed this % increase
*/
function validateCollection(
uint256 _newSupply,
uint256 _interest,
uint256 _timeSinceLastCollection,
uint256 _maxApy,
uint256 _baseApy
) internal pure returns (uint256 extrapolatedAPY) {
uint256 protectedTime = _timeSinceLastCollection == 0 ? 1 : _timeSinceLastCollection;
// Percentage increase in total supply
// e.g. (1e20 * 1e18) / 1e24 = 1e14 (or a 0.01% increase)
// e.g. (5e18 * 1e18) / 1.2e24 = 4.1667e12
// e.g. (1e19 * 1e18) / 1e21 = 1e16
uint256 oldSupply = _newSupply - _interest;
uint256 percentageIncrease = (_interest * 1e18) / oldSupply;
// If over 30 mins, extrapolate APY
// e.g. day: (86400 * 1e18) / 3.154e7 = 2.74..e15
// e.g. 30 mins: (1800 * 1e18) / 3.154e7 = 5.7..e13
// e.g. epoch: (1593596907 * 1e18) / 3.154e7 = 50.4..e18
uint256 yearsSinceLastCollection = (protectedTime * 1e18) / SECONDS_IN_YEAR;
// e.g. 0.01% (1e14 * 1e18) / 2.74..e15 = 3.65e16 or 3.65% apr
// e.g. (4.1667e12 * 1e18) / 5.7..e13 = 7.1e16 or 7.1% apr
// e.g. (1e16 * 1e18) / 50e18 = 2e14
extrapolatedAPY = (percentageIncrease * 1e18) / yearsSinceLastCollection;
if (protectedTime > THIRTY_MINUTES) {
require(extrapolatedAPY < _maxApy, "Interest protected from inflating past maxAPY");
} else {
require(percentageIncrease < _baseApy, "Interest protected from inflating past 10 Bps");
}
}
}
/**
* @title SavingsManager
* @author mStable
* @notice Savings Manager collects interest from mAssets and sends them to the
* corresponding Savings Contract, performing some validation in the process.
* @dev VERSION: 1.4
* DATE: 2021-10-15
*/
contract SavingsManager is ISavingsManager, PausableModule {
using StableMath for uint256;
using SafeERC20 for IERC20;
// Core admin events
event RevenueRecipientSet(address indexed mAsset, address recipient);
event SavingsContractAdded(address indexed mAsset, address savingsContract);
event SavingsContractUpdated(address indexed mAsset, address savingsContract);
event SavingsRateChanged(uint256 newSavingsRate);
event StreamsFrozen();
// Interest collection
event LiquidatorDeposited(address indexed mAsset, uint256 amount);
event InterestCollected(
address indexed mAsset,
uint256 interest,
uint256 newTotalSupply,
uint256 apy
);
event InterestDistributed(address indexed mAsset, uint256 amountSent);
event RevenueRedistributed(address indexed mAsset, address recipient, uint256 amount);
// Locations of each mAsset savings contract
mapping(address => ISavingsContractV2) public savingsContracts;
mapping(address => IRevenueRecipient) public revenueRecipients;
// Time at which last collection was made
mapping(address => uint256) public lastPeriodStart;
mapping(address => uint256) public lastCollection;
mapping(address => uint256) public periodYield;
// Amount of collected interest that will be sent to Savings Contract (1e18 = 100%)
uint256 private savingsRate;
// Streaming liquidated tokens
uint256 private immutable DURATION; // measure in days. eg 1 days or 7 days
uint256 private constant ONE_DAY = 1 days;
uint256 private constant THIRTY_MINUTES = 30 minutes;
// Streams
bool private streamsFrozen = false;
// Liquidator
mapping(address => Stream) public liqStream;
// Platform
mapping(address => Stream) public yieldStream;
// Batches are for the platformInterest collection
mapping(address => uint256) public override lastBatchCollected;
enum StreamType {
liquidator,
yield
}
struct Stream {
uint256 end;
uint256 rate;
}
constructor(
address _nexus,
address[] memory _mAssets,
address[] memory _savingsContracts,
address[] memory _revenueRecipients,
uint256 _savingsRate,
uint256 _duration
) PausableModule(_nexus) {
uint256 len = _mAssets.length;
require(
_savingsContracts.length == len && _revenueRecipients.length == len,
"Invalid inputs"
);
for (uint256 i = 0; i < len; i++) {
_updateSavingsContract(_mAssets[i], _savingsContracts[i]);
emit SavingsContractAdded(_mAssets[i], _savingsContracts[i]);
revenueRecipients[_mAssets[i]] = IRevenueRecipient(_revenueRecipients[i]);
emit RevenueRecipientSet(_mAssets[i], _revenueRecipients[i]);
}
savingsRate = _savingsRate;
DURATION = _duration;
}
modifier onlyLiquidator() {
require(msg.sender == _liquidator(), "Only liquidator can execute");
_;
}
modifier whenStreamsNotFrozen() {
require(!streamsFrozen, "Streaming is currently frozen");
_;
}
/***************************************
STATE
****************************************/
/**
* @dev Adds a new savings contract
* @param _mAsset Address of underlying mAsset
* @param _savingsContract Address of the savings contract
*/
function addSavingsContract(address _mAsset, address _savingsContract) external onlyGovernor {
require(
address(savingsContracts[_mAsset]) == address(0),
"Savings contract already exists"
);
_updateSavingsContract(_mAsset, _savingsContract);
emit SavingsContractAdded(_mAsset, _savingsContract);
}
/**
* @dev Updates an existing savings contract
* @param _mAsset Address of underlying mAsset
* @param _savingsContract Address of the savings contract
*/
function updateSavingsContract(address _mAsset, address _savingsContract)
external
onlyGovernor
{
require(
address(savingsContracts[_mAsset]) != address(0),
"Savings contract does not exist"
);
_updateSavingsContract(_mAsset, _savingsContract);
emit SavingsContractUpdated(_mAsset, _savingsContract);
}
function _updateSavingsContract(address _mAsset, address _savingsContract) internal {
require(_mAsset != address(0) && _savingsContract != address(0), "Must be valid address");
savingsContracts[_mAsset] = ISavingsContractV2(_savingsContract);
IERC20(_mAsset).safeApprove(address(_savingsContract), 0);
IERC20(_mAsset).safeApprove(address(_savingsContract), type(uint256).max);
}
/**
* @dev Freezes streaming of mAssets
*/
function freezeStreams() external onlyGovernor whenStreamsNotFrozen {
streamsFrozen = true;
emit StreamsFrozen();
}
/**
* @dev Sets the revenue recipient address
* @param _mAsset Address of underlying mAsset
* @param _recipient Address of the recipient
*/
function setRevenueRecipient(address _mAsset, address _recipient) external onlyGovernor {
revenueRecipients[_mAsset] = IRevenueRecipient(_recipient);
emit RevenueRecipientSet(_mAsset, _recipient);
}
/**
* @dev Sets a new savings rate for interest distribution
* @param _savingsRate Rate of savings sent to SavingsContract (100% = 1e18)
*/
function setSavingsRate(uint256 _savingsRate) external onlyGovernor {
// Greater than 25% up to 100%
require(_savingsRate >= 25e16 && _savingsRate <= 1e18, "Must be a valid rate");
savingsRate = _savingsRate;
emit SavingsRateChanged(_savingsRate);
}
/**
* @dev Allows the liquidator to deposit proceeds from liquidated gov tokens.
* Transfers proceeds on a second by second basis to the Savings Contract over 1 week.
* @param _mAsset The mAsset to transfer and distribute
* @param _liquidated Units of mAsset to distribute
*/
function depositLiquidation(address _mAsset, uint256 _liquidated)
external
override
whenNotPaused
onlyLiquidator
whenStreamsNotFrozen
{
// Collect existing interest to ensure everything is up to date
_collectAndDistributeInterest(_mAsset);
// transfer liquidated mUSD to here
IERC20(_mAsset).safeTransferFrom(_liquidator(), address(this), _liquidated);
uint256 leftover = _unstreamedRewards(_mAsset, StreamType.liquidator);
_initialiseStream(_mAsset, StreamType.liquidator, _liquidated + leftover, DURATION);
emit LiquidatorDeposited(_mAsset, _liquidated);
}
/**
* @dev Collects the platform interest from a given mAsset and then adds capital to the
* stream. If there is > 24h left in current stream, just top it up, otherwise reset.
* @param _mAsset The mAsset to fetch interest
*/
function collectAndStreamInterest(address _mAsset)
external
override
whenNotPaused
whenStreamsNotFrozen
{
// Collect existing interest to ensure everything is up to date
_collectAndDistributeInterest(_mAsset);
uint256 currentTime = block.timestamp;
uint256 previousBatch = lastBatchCollected[_mAsset];
uint256 timeSincePreviousBatch = currentTime - previousBatch;
require(timeSincePreviousBatch > 6 hours, "Cannot deposit twice in 6 hours");
lastBatchCollected[_mAsset] = currentTime;
// Batch collect
(uint256 interestCollected, uint256 totalSupply) = IMasset(_mAsset)
.collectPlatformInterest();
if (interestCollected > 0) {
// Validate APY
uint256 apy = YieldValidator.validateCollection(
totalSupply,
interestCollected,
timeSincePreviousBatch
);
// Get remaining rewards
uint256 leftover = _unstreamedRewards(_mAsset, StreamType.yield);
_initialiseStream(_mAsset, StreamType.yield, interestCollected + leftover, ONE_DAY);
emit InterestCollected(_mAsset, interestCollected, totalSupply, apy);
} else {
emit InterestCollected(_mAsset, interestCollected, totalSupply, 0);
}
}
/**
* @dev Calculates how many rewards from the stream are still to be distributed, from the
* last collection time to the end of the stream.
* @param _mAsset The mAsset in question
* @return leftover The total amount of mAsset that is yet to be collected from a stream
*/
function _unstreamedRewards(address _mAsset, StreamType _stream)
internal
view
returns (uint256 leftover)
{
uint256 lastUpdate = lastCollection[_mAsset];
Stream memory stream = _stream == StreamType.liquidator
? liqStream[_mAsset]
: yieldStream[_mAsset];
uint256 unclaimedSeconds = 0;
if (lastUpdate < stream.end) {
unclaimedSeconds = stream.end - lastUpdate;
}
return unclaimedSeconds * stream.rate;
}
/**
* @dev Simply sets up the stream
* @param _mAsset The mAsset in question
* @param _amount Amount of units to stream
* @param _duration Duration of the stream, from now
*/
function _initialiseStream(
address _mAsset,
StreamType _stream,
uint256 _amount,
uint256 _duration
) internal {
uint256 currentTime = block.timestamp;
// Distribute reward per second over X seconds
uint256 rate = _amount / _duration;
uint256 end = currentTime + _duration;
if (_stream == StreamType.liquidator) {
liqStream[_mAsset] = Stream(end, rate);
} else {
yieldStream[_mAsset] = Stream(end, rate);
}
// Reset pool data to enable lastCollection usage twice
require(lastCollection[_mAsset] == currentTime, "Stream data must be up to date");
}
/***************************************
COLLECTION
****************************************/
/**
* @dev Collects interest from a target mAsset and distributes to the SavingsContract.
* Applies constraints such that the max APY since the last fee collection cannot
* exceed the "MAX_APY" variable.
* @param _mAsset mAsset for which the interest should be collected
*/
function collectAndDistributeInterest(address _mAsset) external override whenNotPaused {
_collectAndDistributeInterest(_mAsset);
}
function _collectAndDistributeInterest(address _mAsset) internal {
ISavingsContractV2 savingsContract = savingsContracts[_mAsset];
require(address(savingsContract) != address(0), "Must have a valid savings contract");
// Get collection details
uint256 recentPeriodStart = lastPeriodStart[_mAsset];
uint256 previousCollection = lastCollection[_mAsset];
lastCollection[_mAsset] = block.timestamp;
// 1. Collect the new interest from the mAsset
IMasset mAsset = IMasset(_mAsset);
(uint256 interestCollected, uint256 totalSupply) = mAsset.collectInterest();
// 2. Update all the time stamps
// Avoid division by 0 by adding a minimum elapsed time of 1 second
uint256 timeSincePeriodStart = StableMath.max(1, block.timestamp - recentPeriodStart);
uint256 timeSinceLastCollection = StableMath.max(1, block.timestamp - previousCollection);
uint256 inflationOperand = interestCollected;
// If it has been 30 mins since last collection, reset period data
if (timeSinceLastCollection > THIRTY_MINUTES) {
lastPeriodStart[_mAsset] = block.timestamp;
periodYield[_mAsset] = 0;
}
// Else if period has elapsed, start a new period from the lastCollection time
else if (timeSincePeriodStart > THIRTY_MINUTES) {
lastPeriodStart[_mAsset] = previousCollection;
periodYield[_mAsset] = interestCollected;
}
// Else add yield to period yield
else {
inflationOperand = periodYield[_mAsset] + interestCollected;
periodYield[_mAsset] = inflationOperand;
}
// Add on liquidated
uint256 newReward = _unclaimedRewards(_mAsset, previousCollection);
// 3. Validate that interest is collected correctly and does not exceed max APY
if (interestCollected > 0 || newReward > 0) {
require(
IERC20(_mAsset).balanceOf(address(this)) >= interestCollected + newReward,
"Must receive mUSD"
);
uint256 extrapolatedAPY = YieldValidator.validateCollection(
totalSupply,
inflationOperand,
timeSinceLastCollection
);
emit InterestCollected(_mAsset, interestCollected, totalSupply, extrapolatedAPY);
// 4. Distribute the interest
// Calculate the share for savers (95e16 or 95%)
uint256 saversShare = (interestCollected + newReward).mulTruncate(savingsRate);
// Call depositInterest on contract
savingsContract.depositInterest(saversShare);
emit InterestDistributed(_mAsset, saversShare);
} else {
emit InterestCollected(_mAsset, 0, totalSupply, 0);
}
}
/**
* @dev Calculates unclaimed rewards from the liquidation stream
* @param _mAsset mAsset key
* @param _previousCollection Time of previous collection
* @return Units of mAsset that have been unlocked for distribution
*/
function _unclaimedRewards(address _mAsset, uint256 _previousCollection)
internal
view
returns (uint256)
{
Stream memory liq = liqStream[_mAsset];
uint256 unclaimedSeconds_liq = _unclaimedSeconds(_previousCollection, liq.end);
uint256 subtotal_liq = unclaimedSeconds_liq * liq.rate;
Stream memory yield = yieldStream[_mAsset];
uint256 unclaimedSeconds_yield = _unclaimedSeconds(_previousCollection, yield.end);
uint256 subtotal_yield = unclaimedSeconds_yield * yield.rate;
return subtotal_liq + subtotal_yield;
}
/**
* @dev Calculates the seconds of unclaimed rewards, based on period length
* @param _lastUpdate Time of last update
* @param _end End time of period
* @return Seconds of stream that should be compensated
*/
function _unclaimedSeconds(uint256 _lastUpdate, uint256 _end) internal view returns (uint256) {
uint256 currentTime = block.timestamp;
uint256 unclaimedSeconds = 0;
if (currentTime <= _end) {
unclaimedSeconds = currentTime - _lastUpdate;
} else if (_lastUpdate < _end) {
unclaimedSeconds = _end - _lastUpdate;
}
return unclaimedSeconds;
}
/***************************************
Revenue Redistribution
****************************************/
/**
* @dev Redistributes the unallocated interest to the saved recipient, allowing
* the siphoned assets to be used elsewhere in the system
* @param _mAsset mAsset to collect
*/
function distributeUnallocatedInterest(address _mAsset) external override {
IRevenueRecipient recipient = revenueRecipients[_mAsset];
require(address(recipient) != address(0), "Must have valid recipient");
IERC20 mAsset = IERC20(_mAsset);
uint256 balance = mAsset.balanceOf(address(this));
uint256 leftover_liq = _unstreamedRewards(_mAsset, StreamType.liquidator);
uint256 leftover_yield = _unstreamedRewards(_mAsset, StreamType.yield);
uint256 unallocated = balance - leftover_liq - leftover_yield;
mAsset.approve(address(recipient), unallocated);
recipient.notifyRedistributionAmount(_mAsset, unallocated);
emit RevenueRedistributed(_mAsset, address(recipient), unallocated);
}
}
{
"compilationTarget": {
"SavingsManager.sol": "SavingsManager"
},
"evmVersion": "berlin",
"libraries": {},
"metadata": {
"bytecodeHash": "ipfs"
},
"optimizer": {
"enabled": true,
"runs": 200
},
"remappings": []
}
[{"inputs":[{"internalType":"address","name":"_nexus","type":"address"},{"internalType":"address[]","name":"_mAssets","type":"address[]"},{"internalType":"address[]","name":"_savingsContracts","type":"address[]"},{"internalType":"address[]","name":"_revenueRecipients","type":"address[]"},{"internalType":"uint256","name":"_savingsRate","type":"uint256"},{"internalType":"uint256","name":"_duration","type":"uint256"}],"stateMutability":"nonpayable","type":"constructor"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"mAsset","type":"address"},{"indexed":false,"internalType":"uint256","name":"interest","type":"uint256"},{"indexed":false,"internalType":"uint256","name":"newTotalSupply","type":"uint256"},{"indexed":false,"internalType":"uint256","name":"apy","type":"uint256"}],"name":"InterestCollected","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"mAsset","type":"address"},{"indexed":false,"internalType":"uint256","name":"amountSent","type":"uint256"}],"name":"InterestDistributed","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"mAsset","type":"address"},{"indexed":false,"internalType":"uint256","name":"amount","type":"uint256"}],"name":"LiquidatorDeposited","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"address","name":"account","type":"address"}],"name":"Paused","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"mAsset","type":"address"},{"indexed":false,"internalType":"address","name":"recipient","type":"address"}],"name":"RevenueRecipientSet","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"mAsset","type":"address"},{"indexed":false,"internalType":"address","name":"recipient","type":"address"},{"indexed":false,"internalType":"uint256","name":"amount","type":"uint256"}],"name":"RevenueRedistributed","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"mAsset","type":"address"},{"indexed":false,"internalType":"address","name":"savingsContract","type":"address"}],"name":"SavingsContractAdded","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"mAsset","type":"address"},{"indexed":false,"internalType":"address","name":"savingsContract","type":"address"}],"name":"SavingsContractUpdated","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"uint256","name":"newSavingsRate","type":"uint256"}],"name":"SavingsRateChanged","type":"event"},{"anonymous":false,"inputs":[],"name":"StreamsFrozen","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"address","name":"account","type":"address"}],"name":"Unpaused","type":"event"},{"inputs":[{"internalType":"address","name":"_mAsset","type":"address"},{"internalType":"address","name":"_savingsContract","type":"address"}],"name":"addSavingsContract","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"_mAsset","type":"address"}],"name":"collectAndDistributeInterest","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"_mAsset","type":"address"}],"name":"collectAndStreamInterest","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"_mAsset","type":"address"},{"internalType":"uint256","name":"_liquidated","type":"uint256"}],"name":"depositLiquidation","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"_mAsset","type":"address"}],"name":"distributeUnallocatedInterest","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"freezeStreams","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"}],"name":"lastBatchCollected","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"}],"name":"lastCollection","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"}],"name":"lastPeriodStart","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"}],"name":"liqStream","outputs":[{"internalType":"uint256","name":"end","type":"uint256"},{"internalType":"uint256","name":"rate","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"nexus","outputs":[{"internalType":"contract INexus","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"pause","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"paused","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"}],"name":"periodYield","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"}],"name":"revenueRecipients","outputs":[{"internalType":"contract IRevenueRecipient","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"}],"name":"savingsContracts","outputs":[{"internalType":"contract ISavingsContractV2","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"_mAsset","type":"address"},{"internalType":"address","name":"_recipient","type":"address"}],"name":"setRevenueRecipient","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"_savingsRate","type":"uint256"}],"name":"setSavingsRate","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"unpause","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"_mAsset","type":"address"},{"internalType":"address","name":"_savingsContract","type":"address"}],"name":"updateSavingsContract","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"}],"name":"yieldStream","outputs":[{"internalType":"uint256","name":"end","type":"uint256"},{"internalType":"uint256","name":"rate","type":"uint256"}],"stateMutability":"view","type":"function"}]