From 17eca56694e3c205b7938046bc21a6040cf02f5d Mon Sep 17 00:00:00 2001 From: doncesarts Date: Fri, 11 Feb 2022 00:04:04 -0600 Subject: [PATCH 01/14] feat:implements IERC4626Vault on ISavingsContractV3 --- contracts/interfaces/IERC4626Vault.sol | 153 ++++++++ contracts/interfaces/ISavingsContract.sol | 27 +- contracts/savings/SavingsContract.sol | 345 +++++++++++++++--- .../z_mocks/savings/MockSavingsContract.sol | 2 +- 4 files changed, 478 insertions(+), 49 deletions(-) create mode 100644 contracts/interfaces/IERC4626Vault.sol diff --git a/contracts/interfaces/IERC4626Vault.sol b/contracts/interfaces/IERC4626Vault.sol new file mode 100644 index 00000000..00c13570 --- /dev/null +++ b/contracts/interfaces/IERC4626Vault.sol @@ -0,0 +1,153 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later +pragma solidity 0.8.6; +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; + +/// @title Yield Bearing Vault +interface IERC4626Vault { + /** + * @dev Must be an ERC-20 token contract. Must not revert. + * + * Returns the address of the underlying token used for the Vault uses for accounting, depositing, and withdrawing + */ + function asset() external view returns (address assetTokenAddress); + + /** + * @dev It should include any compounding that occurs from yield. It must be inclusive of any fees that are charged against assets in the Vault. It must not revert. + * + * Returns the total amount of the underlying asset that is “managed” by Vault. + */ + function totalAssets() external view returns (uint256 totalManagedAssets); + + /** + * @dev It must be inclusive of any fees that are charged against assets in the Vault. + * + * Returns the current exchange rate of shares to assets, quoted per unit share (share unit is 10 ** Vault.decimals()). + */ + function assetsPerShare() external view returns (uint256 assetsPerUnitShare); + + /** + * @dev It MAY be more accurate than using assetsPerShare or totalAssets / Vault.totalSupply for certain types of fee calculations. + * + * Returns the total number of underlying assets that depositor’s shares represent. + */ + function assetsOf(address depositor) external view returns (uint256 assets); + + /** + * @dev It must return a limited value if caller is subject to some deposit limit. must return 2 ** 256 - 1 if there is no limit on the maximum amount of assets that may be deposited. + * + * Returns the total number of underlying assets that caller can be deposit. + */ + function maxDeposit(address caller) external view returns (uint256 maxAssets); + + /** + * @dev It must return the exact amount of Vault shares that would be minted if the caller were to deposit a given exact amount of underlying assets using the deposit method. + * + * It simulate the effects of their deposit at the current block, given current on-chain conditions. + * Returns the amount of shares. + */ + function previewDeposit(uint256 assets) external view returns (uint256 shares); + + /** + * + * Mints shares Vault shares to receiver by depositing exactly amount of underlying tokens. + * Returns the amount of shares minted. + * Emits a {Deposit} event. + */ + function deposit(uint256 assets, address receiver) external returns (uint256 shares); + + /** + * @dev must return a limited value if caller is subject to some deposit limit. must return 2 ** 256 - 1 if there is no limit on the maximum amount of shares that may be minted + * + * Returns Total number of underlying shares that caller can be mint. + */ + function maxMint(address caller) external view returns (uint256 maxShares); + + /** + * @dev Allows an on-chain or off-chain user to simulate the effects of their mint at the current block, given current on-chain conditions. + * + * Returns Total number of underlying shares to be minted. + */ + function previewMint(uint256 shares) external view returns (uint256 assets); + + /** + * Mints exactly shares Vault shares to receiver by depositing amount of underlying tokens. + * + * Returns Total number of underlying shares that caller mint. + * Emits a {Deposit} event. + */ + function mint(uint256 shares, address receiver) external returns (uint256 assets); + + /** + * + * Returns Total number of underlying assets that caller can withdraw. + */ + function maxWithdraw(address caller) external view returns (uint256 maxAssets); + + /** + * @dev Allows an on-chain or off-chain user to simulate the effects of their withdrawal at the current block, given current on-chain conditions. + * + * Return the exact amount of Vault shares that would be redeemed by the caller if withdrawing a given exact amount of underlying assets using the withdraw method. + */ + function previewWithdraw(uint256 assets) external view returns (uint256 shares); + + /** + * Redeems shares from owner and sends assets of underlying tokens to receiver. + * Returns Total number of underlying shares redeemed. + * Emits a {Withdraw} event. + */ + function withdraw( + uint256 assets, + address receiver, + address owner + ) external returns (uint256 shares); + + /** + * @dev it must return a limited value if caller is subject to some withdrawal limit or timelock. must return balanceOf(caller) if caller is not subject to any withdrawal limit or timelock. MAY be used in the previewRedeem or redeem methods for shares input parameter. must NOT revert. + * + * Returns Total number of underlying shares that caller can redeem. + */ + function maxRedeem(address caller) external view returns (uint256); + + /** + * @dev Allows an on-chain or off-chain user to simulate the effects of their redeemption at the current block, given current on-chain conditions. + * + * Returns the exact amount of underlying assets that would be withdrawn by the caller if redeeming a given exact amount of Vault shares using the redeem method + */ + function previewRedeem(uint256 shares) external view returns (uint256 assets); + + /** + * Redeems shares from owner and sends assets of underlying tokens to receiver. + * + * Returns Total number of underlying assets of underlying redeemed. + * Emits a {Withdraw} event. + */ + function redeem( + uint256 shares, + address receiver, + address owner + ) external returns (uint256 assets); + + /*/////////////////////////////////////////////////////////////// + Events + //////////////////////////////////////////////////////////////*/ + + /** + * @dev Emitted when sender has exchanged assets for shares, and transferred those shares to receiver. + * + * Note It must be emitted when tokens are deposited into the Vault in ERC4626.mint or ERC4626.deposit methods. + * + */ + event Deposit(address indexed sender, address indexed receiver, uint256 assets); + /** + * @dev Emitted when sender has exchanged shares for assets, and transferred those assets to receiver. + * + * Note It must be emitted when shares are withdrawn from the Vault in ERC4626.redeem or ERC4626.withdraw methods. + * + */ + event Withdraw( + address indexed sender, + address indexed receiver, + uint256 assets, + uint256 shares + ); +} diff --git a/contracts/interfaces/ISavingsContract.sol b/contracts/interfaces/ISavingsContract.sol index 8833915d..417c2745 100644 --- a/contracts/interfaces/ISavingsContract.sol +++ b/contracts/interfaces/ISavingsContract.sol @@ -46,16 +46,16 @@ interface ISavingsContractV2 { function underlying() external view returns (IERC20 underlyingMasset); // V2 } -interface ISavingsContractV3 { +interface ISavingsContractV3 is IERC4626Vault { // 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 - + /*/////////////////////////////////////////////////////////////// + DEPRECATED for IERC4626Vault + //////////////////////////////////////////////////////////////*/ + /** @dev Deprecated in favour of IERC4626Vault.deposit(uint256 assets, address receiver)*/ function depositSavings(uint256 _amount) external returns (uint256 creditsIssued); // V1 & V2 function depositSavings(uint256 _amount, address _beneficiary) @@ -64,6 +64,18 @@ interface ISavingsContractV3 { function redeemCredits(uint256 _amount) external returns (uint256 underlyingReturned); // V2 + /** @dev Deprecated in favour of IERC4626Vault.withdraw(uint256 assets,address receiver,address owner)(uint256 assets, address receiver)*/ + function redeemUnderlying(uint256 _amount) external returns (uint256 creditsBurned); // V2 + + /** @dev Deprecated in favour of IERC4626Vault.assetsPerShare() external view returns (uint256 assetsPerUnitShare);*/ + function exchangeRate() external view returns (uint256); // V1 & V2 + + /** @dev Deprecated in favour of IERC4626Vault.assetsOf(addresss depositor) view returns (uint256 assets)*/ + function balanceOfUnderlying(address _user) external view returns (uint256 underlying); // V2 + + /** @dev Deprecated in favour of IERC4626Vault.asset()(address assetTokenAddress);*/ + function underlying() external view returns (IERC20 underlyingMasset); // V2 + function redeemUnderlying(uint256 _amount) external returns (uint256 creditsBurned); // V2 function exchangeRate() external view returns (uint256); // V1 & V2 @@ -77,6 +89,11 @@ interface ISavingsContractV3 { function underlying() external view returns (IERC20 underlyingMasset); // V2 // -------------------------------------------- + function deposit( + uint256 assets, + address receiver, + address referrer + ) external returns (uint256 creditsIssued); // V3 function redeemAndUnwrap( uint256 _amount, diff --git a/contracts/savings/SavingsContract.sol b/contracts/savings/SavingsContract.sol index 7f699d37..a0b6ef6b 100644 --- a/contracts/savings/SavingsContract.sol +++ b/contracts/savings/SavingsContract.sol @@ -14,6 +14,8 @@ import { Initializable } from "../shared/@openzeppelin-2.5/Initializable.sol"; // Libs import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import { StableMath } from "../shared/StableMath.sol"; import { YieldValidator } from "../shared/YieldValidator.sol"; @@ -23,11 +25,12 @@ import { YieldValidator } from "../shared/YieldValidator.sol"; * @notice Savings contract uses the ever increasing "exchangeRate" to increase * the value of the Savers "credits" (ERC20) relative to the amount of additional * underlying collateral that has been deposited into this contract ("interest") - * @dev VERSION: 2.1 - * DATE: 2021-11-25 + * @dev VERSION: 2.2 + * DATE: 2021-02-08 */ contract SavingsContract is ISavingsContractV3, Initializable, InitializableToken, ImmutableModule { using StableMath for uint256; + using SafeERC20 for IERC20; // Core events for depositing and withdrawing event ExchangeRateUpdated(uint256 newExchangeRate, uint256 interestCollected); @@ -79,6 +82,7 @@ contract SavingsContract is ISavingsContractV3, Initializable, InitializableToke // Max APY generated on the capital in the connector uint256 private constant MAX_APY = 4e18; uint256 private constant SECONDS_IN_YEAR = 365 days; + uint256 private constant MAX_INT = 2**256 - 1; // Proxy contract for easy redemption address public immutable unwrapper; @@ -120,6 +124,7 @@ contract SavingsContract is ISavingsContractV3, Initializable, InitializableToke ****************************************/ /** + * @dev Deprecated in favour of IERC4626Vault.assetsOf(addresss depositor) * @notice Returns the underlying balance of a given user * @param _user Address of the user to check * @return balance Units of underlying owned by the user. eg mUSD or mBTC @@ -151,9 +156,11 @@ contract SavingsContract is ISavingsContractV3, Initializable, InitializableToke (amount, ) = _creditsToUnderlying(_credits); } - // Deprecated in favour of `balanceOf(address)` - // Maintained for backwards compatibility - // Returns the credit balance of a given user + /** + * @dev Deprecated in favour of `balanceOf(address)` + * Maintained for backwards compatibility + * Returns the credit balance of a given user + **/ function creditBalances(address _user) external view override returns (uint256) { return balanceOf(_user); } @@ -170,23 +177,7 @@ contract SavingsContract is ISavingsContractV3, Initializable, InitializableToke * @param _amount Units of underlying to add to the savings vault */ function depositInterest(uint256 _amount) external override onlySavingsManager { - require(_amount > 0, "Must deposit something"); - - // Transfer the interest from sender to here - require(underlying.transferFrom(msg.sender, address(this), _amount), "Must receive tokens"); - - // Calc new exchange rate, protect against initialisation case - uint256 totalCredits = totalSupply(); - if (totalCredits > 0) { - // new exchange rate is relationship between _totalCredits & totalSavings - // _totalCredits * exchangeRate = totalSavings - // exchangeRate = totalSavings/_totalCredits - (uint256 totalCollat, ) = _creditsToUnderlying(totalCredits); - uint256 newExchangeRate = _calcExchangeRate(totalCollat + _amount, totalCredits); - exchangeRate = newExchangeRate; - - emit ExchangeRateUpdated(newExchangeRate, _amount); - } + _mint(_amount, msg.sender, false); } /** @notice Enable or disable the automation of fee collection during deposit process */ @@ -214,6 +205,8 @@ contract SavingsContract is ISavingsContractV3, Initializable, InitializableToke } /** + * @dev Deprecated in favour of IERC4626Vault.deposit(uint256 assets, address receiver) + * * @notice Deposit the senders savings to the vault, and credit them internally with "credits". * Credit amount is calculated as a ratio of deposit amount and exchange rate: * credits = underlying / exchangeRate @@ -226,6 +219,8 @@ contract SavingsContract is ISavingsContractV3, Initializable, InitializableToke } /** + * @dev Deprecated in favour of IERC4626Vault.deposit(uint256 assets, address receiver) + * * @notice Deposit the senders savings to the vault, and credit them internally with "credits". * Credit amount is calculated as a ratio of deposit amount and exchange rate: * credits = underlying / exchangeRate @@ -243,6 +238,8 @@ contract SavingsContract is ISavingsContractV3, Initializable, InitializableToke } /** + * @dev Deprecated in favour of IERC4626Vault.deposit(uint256 assets, address receiver, address _referrer) + * * @notice Overloaded `depositSavings` method with an optional referrer address. * @param _underlying Units of underlying to deposit into savings vault. eg mUSD or mBTC * @param _beneficiary Address to the new credits will be issued to. @@ -285,6 +282,7 @@ contract SavingsContract is ISavingsContractV3, Initializable, InitializableToke _mint(_beneficiary, creditsIssued); emit SavingsDeposited(_beneficiary, _underlying, creditsIssued); + emit Deposit(msg.sender, _beneficiary, _underlying); } /*************************************** @@ -318,10 +316,7 @@ contract SavingsContract is ISavingsContractV3, Initializable, InitializableToke function redeemCredits(uint256 _credits) external override returns (uint256 massetReturned) { require(_credits > 0, "Must withdraw something"); - // Collect recent interest generated by basket and update exchange rate - if (automateInterestCollection) { - ISavingsManager(_savingsManager()).collectAndDistributeInterest(address(underlying)); - } + _beforeRedeem(); (, uint256 payout) = _redeem(_credits, true, true); @@ -340,18 +335,7 @@ contract SavingsContract is ISavingsContractV3, Initializable, InitializableToke override returns (uint256 creditsBurned) { - require(_underlying > 0, "Must withdraw something"); - - // Collect recent interest generated by basket and update exchange rate - if (automateInterestCollection) { - ISavingsManager(_savingsManager()).collectAndDistributeInterest(address(underlying)); - } - - // Ensure that the payout was sufficient - (uint256 credits, uint256 massetReturned) = _redeem(_underlying, false, true); - require(massetReturned == _underlying, "Invalid output"); - - return credits; + return _withdraw(_underlying, msg.sender, msg.sender); } /** @@ -395,10 +379,7 @@ contract SavingsContract is ISavingsContractV3, Initializable, InitializableToke require(_beneficiary != address(0), "Beneficiary address is zero"); require(_router != address(0), "Router address is zero"); - // Collect recent interest generated by basket and update exchange rate - if (automateInterestCollection) { - ISavingsManager(_savingsManager()).collectAndDistributeInterest(address(underlying)); - } + _beforeRedeem(); // Ensure that the payout was sufficient (creditsBurned, massetReturned) = _redeem(_amount, _isCreditAmt, false); @@ -422,6 +403,15 @@ contract SavingsContract is ISavingsContractV3, Initializable, InitializableToke ); } + /** + * @dev Internally call before redeeming credits. It collects recent interest generated by basket and update exchange rate. + */ + function _beforeRedeem() internal { + if (automateInterestCollection) { + ISavingsManager(_savingsManager()).collectAndDistributeInterest(address(underlying)); + } + } + /** * @dev Internally burn the credits and optionally send the underlying to msg.sender */ @@ -429,6 +419,19 @@ contract SavingsContract is ISavingsContractV3, Initializable, InitializableToke uint256 _amt, bool _isCreditAmt, bool _transferUnderlying + ) internal returns (uint256 creditsBurned, uint256 massetReturned) { + return _redeem(_amt, msg.sender, msg.sender, _isCreditAmt, _transferUnderlying); + } + + /** + * @dev Internally burn the credits and optionally send the underlying to msg.sender + */ + function _redeem( + uint256 _amt, + address receiver, + address owner, + bool _isCreditAmt, + bool _transferUnderlying ) internal returns (uint256 creditsBurned, uint256 massetReturned) { // Centralise credit <> underlying calcs and minimise SLOAD count uint256 credits_; @@ -450,7 +453,8 @@ contract SavingsContract is ISavingsContractV3, Initializable, InitializableToke // Optionally, transfer tokens from here to sender if (_transferUnderlying) { - require(underlying.transfer(msg.sender, underlying_), "Must send tokens"); + require(underlying.safeTransfer(receiver, underlying_), "Must send tokens"); + emit Withdraw(receiver, owner, underlying_, credits_); } // If this withdrawal pushes the portion of stored collateral in the `connector` over a certain @@ -710,6 +714,8 @@ contract SavingsContract is ISavingsContractV3, Initializable, InitializableToke view returns (uint256 credits, uint256 exchangeRate_) { + // TODO - change _underlying to assets? + // TODO - change _underlyingToCredits to _calculateShares? // e.g. (1e20 * 1e18) / 1e18 = 1e20 // e.g. (1e20 * 1e18) / 14e17 = 7.1429e19 // e.g. 1 * 1e18 / 1e17 + 1 = 11 => 11 * 1e17 / 1e18 = 1.1e18 / 1e18 = 1 @@ -738,9 +744,262 @@ contract SavingsContract is ISavingsContractV3, Initializable, InitializableToke view returns (uint256 underlyingAmount, uint256 exchangeRate_) { + // TODO - change _creditsToUnderlying to _calculateAssets? + // TODO - change _credits to shares? // e.g. (1e20 * 1e18) / 1e18 = 1e20 // e.g. (1e20 * 14e17) / 1e18 = 1.4e20 exchangeRate_ = exchangeRate; underlyingAmount = _credits.mulTruncate(exchangeRate_); } + + /*/////////////////////////////////////////////////////////////// + IERC4626Vault + //////////////////////////////////////////////////////////////*/ + + /** + * @notice it must be an ERC-20 token contract. Must not revert. + * + * Returns the address of the underlying token used for the Vault uses for accounting, depositing, and withdrawing. + */ + function asset() external view override returns (address assetTokenAddress) { + return address(underlying); + } + + /** + * @notice It should include any compounding that occurs from yield. It must be inclusive of any fees that are charged against assets in the Vault. It must not revert. + * + * Returns the total amount of the underlying asset that is “managed” by Vault. + */ + function totalAssets() external view override returns (uint256 totalManagedAssets) { + return underlying.balanceOf(address(this)); + } + + /** + * @notice It must be inclusive of any fees that are charged against assets in the Vault. + * + * Returns the current exchange rate of shares to assets, quoted per unit share (share unit is 10 ** Vault.decimals()). + */ + function assetsPerShare() external view override returns (uint256 assetsPerUnitShare) { + return exchangeRate; + } + + /** + * @notice Returns the underlying assets balance of a given depositor + * @param depositor Address of the user to check + * @return assets Units of underlying assets owned by the depositor. eg mUSD or mBTC + + * Returns the total number of underlying assets that depositor’s shares represent. + */ + function assetsOf(address depositor) external view override returns (uint256 assets) { + (assets, ) = _creditsToUnderlying(balanceOf(depositor)); + } + + /** + * @notice It must return a limited value if caller is subject to some deposit limit. must return 2 ** 256 - 1 if there is no limit on the maximum amount of assets that may be deposited. + * + * Returns the total number of underlying assets that caller can be deposit. + */ + function maxDeposit(address caller) external pure override returns (uint256 maxAssets) { + return MAX_INT; + } + + /** + * @notice It must return the exact amount of Vault shares that would be minted if the caller were to deposit a given exact amount of underlying assets using the deposit method. + * + * It simulate the effects of their deposit at the current block, given current on-chain conditions. + * Returns the amount of shares. + */ + function previewDeposit(uint256 assets) external view override returns (uint256 shares) { + // TODO - change _underlyingToCredits to _calculateShares? + (shares, ) = _underlyingToCredits(assets); + } + + /** + * @notice Deposit the senders savings to the vault, and credit them internally with "credits". + * Credit amount is calculated as a ratio of deposit amount and exchange rate: + * credits = underlying / exchangeRate + * We will first update the internal exchange rate by collecting any interest generated on the underlying. + * Emits a {Deposit} event. + * @param assets Units of underlying to deposit into savings vault. eg mUSD or mBTC + * @param receiver The address to receive the Vault shares. + * @return shares Units of credits issued. eg imUSD or imBTC + */ + function deposit(uint256 assets, address receiver) external override returns (uint256 shares) { + return _deposit(assets, receiver, true); + } + + /** + * + * @notice Overloaded `deposit` method with an optional referrer address. + * @param assets Units of underlying to deposit into savings vault. eg mUSD or mBTC + * @param receiver Address to the new credits will be issued to. + * @param referrer Referrer address for this deposit. + * @return shares Units of credits issued. eg imUSD or imBTC + */ + function deposit( + uint256 assets, + address receiver, + address referrer + ) external override returns (uint256 shares) { + emit Referral(referrer, receiver, assets); + return _deposit(assets, receiver, true); + } + + /** + * @notice must return a limited value if caller is subject to some deposit limit. must return 2 ** 256 - 1 if there is no limit on the maximum amount of shares that may be minted + * + * Returns Total number of underlying shares that caller can be mint. + */ + function maxMint(address caller) external view override returns (uint256 maxShares) { + maxShares = balanceOf(caller); + return maxShares; + } + + /** + * @notice Allows an on-chain or off-chain user to simulate the effects of their mint at the current block, given current on-chain conditions. + * + * Returns Total number of underlying shares to be minted. + */ + function previewMint(uint256 shares) external view override returns (uint256 assets) { + (assets, ) = _creditsToUnderlying(shares); + // TODO - review when Nothing deposit yet + // if( assets == 0 && balanceOf(address(this)) == 0 ) { + // return shares * exchangeRate_; + // } + return assets; + } + + /** + * Mints exactly shares Vault shares to receiver by depositing amount of underlying tokens. + * + * Returns Total number of underlying shares that caller mint. + * Emits a {Deposit} event. + */ + function mint(uint256 shares, address receiver) external override returns (uint256 assets) { + assets = _mint(shares, receiver, true); + return assets; + } + + function _mint( + uint256 _amount, + address receiver, + bool _isCreditAmt + ) internal returns (uint256 assets) { + require(_amount > 0, "Must deposit something"); + + if (_isCreditAmt) { + (assets, ) = _creditsToUnderlying(_amount); + } else { + assets = _amount; + } + // Transfer the interest from sender to here + require(underlying.transferFrom(msg.sender, address(this), assets), "Must receive tokens"); + + if (_isCreditAmt) { + _mint(receiver, assets); + emit Deposit(msg.sender, receiver, assets); + } + // Calc new exchange rate, protect against initialisation case + uint256 totalCredits = totalSupply(); + if (totalCredits > 0) { + // new exchange rate is relationship between _totalCredits & totalSavings + // _totalCredits * exchangeRate = totalSavings + // exchangeRate = totalSavings/_totalCredits + (uint256 totalCollat, ) = _creditsToUnderlying(totalCredits); + uint256 newExchangeRate = _calcExchangeRate(totalCollat + assets, totalCredits); + exchangeRate = newExchangeRate; + + emit ExchangeRateUpdated(newExchangeRate, assets); + } + + return (assets); + } + + /** + * + * Returns Total number of underlying assets that caller can withdraw. + */ + function maxWithdraw(address caller) external view override returns (uint256 maxAssets) { + (maxAssets, ) = _creditsToUnderlying(balanceOf(caller)); + return maxAssets; + } + + /** + * @notice Allows an on-chain or off-chain user to simulate the effects of their withdrawal at the current block, given current on-chain conditions. + * + * Return the exact amount of Vault shares that would be redeemed by the caller if withdrawing a given exact amount of underlying assets using the withdraw method. + */ + function previewWithdraw(uint256 assets) external view override returns (uint256 shares) { + (shares, ) = _underlyingToCredits(assets); + return shares; + } + + /** + * Redeems shares from owner and sends assets of underlying tokens to receiver. + * Returns Total number of underlying shares redeemed. + * Emits a {Withdraw} event. + */ + function withdraw( + uint256 assets, + address receiver, + address owner + ) external override returns (uint256 shares) { + return _withdraw(assets, receiver, owner); + } + + /** + * @notice it must return a limited value if caller is subject to some withdrawal limit or timelock. must return balanceOf(caller) if caller is not subject to any withdrawal limit or timelock. MAY be used in the previewRedeem or redeem methods for shares input parameter. must NOT revert. + * + * Returns Total number of underlying shares that caller can redeem. + */ + function maxRedeem(address caller) external view override returns (uint256 maxShares) { + maxShares = balanceOf(caller); + return maxShares; + } + + /** + * @notice Allows an on-chain or off-chain user to simulate the effects of their redeemption at the current block, given current on-chain conditions. + * + * Returns the exact amount of underlying assets that would be withdrawn by the caller if redeeming a given exact amount of Vault shares using the redeem method + */ + function previewRedeem(uint256 shares) external view override returns (uint256 assets) { + (assets, ) = _creditsToUnderlying(shares); + return assets; + } + + /** + * Redeems shares from owner and sends assets of underlying tokens to receiver. + * + * Returns Total number of underlying assets of underlying redeemed. + * Emits a {Withdraw} event. + */ + function redeem( + uint256 shares, + address receiver, + address owner + ) external override returns (uint256 assets) { + require(shares > 0, "Must withdraw something"); + + _beforeRedeem(); + + (, assets) = _redeem(shares, receiver, owner, true, true); + + return assets; + } + + function _withdraw( + uint256 assets, + address receiver, + address owner + ) internal returns (uint256 shares) { + require(assets > 0, "Must withdraw something"); + + _beforeRedeem(); + + // Ensure that the payout was sufficient + (uint256 credits, uint256 massetReturned) = _redeem(assets, receiver, owner, false, true); + require(massetReturned == assets, "Invalid output"); + + return credits; + } } diff --git a/contracts/z_mocks/savings/MockSavingsContract.sol b/contracts/z_mocks/savings/MockSavingsContract.sol index 17d0fd25..ff0b37c6 100644 --- a/contracts/z_mocks/savings/MockSavingsContract.sol +++ b/contracts/z_mocks/savings/MockSavingsContract.sol @@ -88,7 +88,7 @@ contract MockSavingsContract is ERC20 { address _output, address _beneficiary, address _router, - bool + bool _isBassetOut ) external returns ( From b28b71f04ee5a7b8b0d94932c1fc69a5d1f8b0c8 Mon Sep 17 00:00:00 2001 From: doncesarts Date: Fri, 11 Feb 2022 00:33:53 -0600 Subject: [PATCH 02/14] fix: compilation error on SavingsContract --- contracts/savings/SavingsContract.sol | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/contracts/savings/SavingsContract.sol b/contracts/savings/SavingsContract.sol index a0b6ef6b..f21bdea3 100644 --- a/contracts/savings/SavingsContract.sol +++ b/contracts/savings/SavingsContract.sol @@ -14,7 +14,6 @@ import { Initializable } from "../shared/@openzeppelin-2.5/Initializable.sol"; // Libs import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; -import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import { StableMath } from "../shared/StableMath.sol"; import { YieldValidator } from "../shared/YieldValidator.sol"; @@ -453,7 +452,7 @@ contract SavingsContract is ISavingsContractV3, Initializable, InitializableToke // Optionally, transfer tokens from here to sender if (_transferUnderlying) { - require(underlying.safeTransfer(receiver, underlying_), "Must send tokens"); + underlying.safeTransfer(receiver, underlying_); emit Withdraw(receiver, owner, underlying_, credits_); } From afb851fb281401d95826629b72f39dd5d3e9edd6 Mon Sep 17 00:00:00 2001 From: doncesarts Date: Mon, 14 Feb 2022 10:26:10 -0600 Subject: [PATCH 03/14] chore: removes comment from sm --- contracts/savings/SavingsContract.sol | 89 ++++++++++++-------------- test-fork/savings/sm14-upgrade.spec.ts | 1 - 2 files changed, 40 insertions(+), 50 deletions(-) diff --git a/contracts/savings/SavingsContract.sol b/contracts/savings/SavingsContract.sol index f21bdea3..1f1f0eab 100644 --- a/contracts/savings/SavingsContract.sol +++ b/contracts/savings/SavingsContract.sol @@ -27,7 +27,7 @@ import { YieldValidator } from "../shared/YieldValidator.sol"; * @dev VERSION: 2.2 * DATE: 2021-02-08 */ -contract SavingsContract is ISavingsContractV3, Initializable, InitializableToken, ImmutableModule { +contract SavingsContract is ISavingsContractV3, Initializable, InitializableToken, ImmutablexModule { using StableMath for uint256; using SafeERC20 for IERC20; @@ -627,9 +627,9 @@ contract SavingsContract is ISavingsContractV3, Initializable, InitializableToke uint256 ideal = sum.mulTruncate(_data.fraction); // If there is not enough mAsset in the connector, then deposit if (ideal > connectorBalance) { - uint256 deposit = ideal - connectorBalance; - underlying.approve(address(connector_), deposit); - connector_.deposit(deposit); + uint256 deposit_ = ideal - connectorBalance; + underlying.approve(address(connector_), deposit_); + connector_.deposit(deposit_); } // Else withdraw, if there is too much mAsset in the connector else if (connectorBalance > ideal) { @@ -713,8 +713,6 @@ contract SavingsContract is ISavingsContractV3, Initializable, InitializableToke view returns (uint256 credits, uint256 exchangeRate_) { - // TODO - change _underlying to assets? - // TODO - change _underlyingToCredits to _calculateShares? // e.g. (1e20 * 1e18) / 1e18 = 1e20 // e.g. (1e20 * 1e18) / 14e17 = 7.1429e19 // e.g. 1 * 1e18 / 1e17 + 1 = 11 => 11 * 1e17 / 1e18 = 1.1e18 / 1e18 = 1 @@ -743,8 +741,6 @@ contract SavingsContract is ISavingsContractV3, Initializable, InitializableToke view returns (uint256 underlyingAmount, uint256 exchangeRate_) { - // TODO - change _creditsToUnderlying to _calculateAssets? - // TODO - change _credits to shares? // e.g. (1e20 * 1e18) / 1e18 = 1e20 // e.g. (1e20 * 14e17) / 1e18 = 1.4e20 exchangeRate_ = exchangeRate; @@ -798,7 +794,7 @@ contract SavingsContract is ISavingsContractV3, Initializable, InitializableToke * * Returns the total number of underlying assets that caller can be deposit. */ - function maxDeposit(address caller) external pure override returns (uint256 maxAssets) { + function maxDeposit(address) external pure override returns (uint256 maxAssets) { return MAX_INT; } @@ -809,7 +805,6 @@ contract SavingsContract is ISavingsContractV3, Initializable, InitializableToke * Returns the amount of shares. */ function previewDeposit(uint256 assets) external view override returns (uint256 shares) { - // TODO - change _underlyingToCredits to _calculateShares? (shares, ) = _underlyingToCredits(assets); } @@ -862,9 +857,6 @@ contract SavingsContract is ISavingsContractV3, Initializable, InitializableToke function previewMint(uint256 shares) external view override returns (uint256 assets) { (assets, ) = _creditsToUnderlying(shares); // TODO - review when Nothing deposit yet - // if( assets == 0 && balanceOf(address(this)) == 0 ) { - // return shares * exchangeRate_; - // } return assets; } @@ -878,42 +870,6 @@ contract SavingsContract is ISavingsContractV3, Initializable, InitializableToke assets = _mint(shares, receiver, true); return assets; } - - function _mint( - uint256 _amount, - address receiver, - bool _isCreditAmt - ) internal returns (uint256 assets) { - require(_amount > 0, "Must deposit something"); - - if (_isCreditAmt) { - (assets, ) = _creditsToUnderlying(_amount); - } else { - assets = _amount; - } - // Transfer the interest from sender to here - require(underlying.transferFrom(msg.sender, address(this), assets), "Must receive tokens"); - - if (_isCreditAmt) { - _mint(receiver, assets); - emit Deposit(msg.sender, receiver, assets); - } - // Calc new exchange rate, protect against initialisation case - uint256 totalCredits = totalSupply(); - if (totalCredits > 0) { - // new exchange rate is relationship between _totalCredits & totalSavings - // _totalCredits * exchangeRate = totalSavings - // exchangeRate = totalSavings/_totalCredits - (uint256 totalCollat, ) = _creditsToUnderlying(totalCredits); - uint256 newExchangeRate = _calcExchangeRate(totalCollat + assets, totalCredits); - exchangeRate = newExchangeRate; - - emit ExchangeRateUpdated(newExchangeRate, assets); - } - - return (assets); - } - /** * * Returns Total number of underlying assets that caller can withdraw. @@ -985,6 +941,40 @@ contract SavingsContract is ISavingsContractV3, Initializable, InitializableToke return assets; } + function _mint( + uint256 _amount, + address receiver, + bool _isCreditAmt + ) internal returns (uint256 assets) { + require(_amount > 0, "Must deposit something"); + + if (_isCreditAmt) { + (assets, ) = _creditsToUnderlying(_amount); + } else { + assets = _amount; + } + // Transfer the interest from sender to here + require(underlying.transferFrom(msg.sender, address(this), assets), "Must receive tokens"); + + if (_isCreditAmt) { + _mint(receiver, assets); + emit Deposit(msg.sender, receiver, assets); + } + // Calc new exchange rate, protect against initialisation case + uint256 totalCredits = totalSupply(); + if (totalCredits > 0) { + // new exchange rate is relationship between _totalCredits & totalSavings + // _totalCredits * exchangeRate = totalSavings + // exchangeRate = totalSavings/_totalCredits + (uint256 totalCollat, ) = _creditsToUnderlying(totalCredits); + uint256 newExchangeRate = _calcExchangeRate(totalCollat + assets, totalCredits); + exchangeRate = newExchangeRate; + + emit ExchangeRateUpdated(newExchangeRate, assets); + } + + return (assets); + } function _withdraw( uint256 assets, @@ -994,6 +984,7 @@ contract SavingsContract is ISavingsContractV3, Initializable, InitializableToke require(assets > 0, "Must withdraw something"); _beforeRedeem(); + // if (msg.sender != owner && allowed != type(uint256).max) allowance[owner][msg.sender] = allowed - shares; // Ensure that the payout was sufficient (uint256 credits, uint256 massetReturned) = _redeem(assets, receiver, owner, false, true); diff --git a/test-fork/savings/sm14-upgrade.spec.ts b/test-fork/savings/sm14-upgrade.spec.ts index 4f501856..193908e2 100644 --- a/test-fork/savings/sm14-upgrade.spec.ts +++ b/test-fork/savings/sm14-upgrade.spec.ts @@ -18,7 +18,6 @@ import { import { Account } from "types" import { Chain, COMP, USDC, USDT, WBTC } from "tasks/utils/tokens" import { resolveAddress } from "../../tasks/utils/networkAddressFactory" -import { deployContract } from "../../tasks/utils/deploy-utils" const musdWhaleAddress = "0x136d841d4bece3fc0e4debb94356d8b6b4b93209" const governorAddress = resolveAddress("Governor") From 094940ba85017f32a173c8f0f51cc01bb28c3dcb Mon Sep 17 00:00:00 2001 From: doncesarts Date: Thu, 7 Apr 2022 03:14:36 +0100 Subject: [PATCH 04/14] feat: implements 4626 on SavingsContract --- .eslintrc.js | 2 + .solcover.js | 2 + contracts/interfaces/IERC4626Vault.sol | 131 +- contracts/interfaces/ISavingsContract.sol | 35 +- .../legacy-upgraded/imbtc-mainnet-22.sol | 1681 +++++++++++++++ .../legacy-upgraded/imusd-mainnet-22.sol | 1859 +++++++++++++++++ .../legacy-upgraded/imusd-polygon-22.sol | 1699 +++++++++++++++ contracts/savings/SavingsContract.sol | 339 +-- package.json | 4 +- tasks/save.ts | 2 +- test/savings/savings-contract.spec.ts | 10 +- yarn.lock | 25 +- 12 files changed, 5523 insertions(+), 266 deletions(-) create mode 100644 contracts/legacy-upgraded/imbtc-mainnet-22.sol create mode 100644 contracts/legacy-upgraded/imusd-mainnet-22.sol create mode 100644 contracts/legacy-upgraded/imusd-polygon-22.sol diff --git a/.eslintrc.js b/.eslintrc.js index bfa4872d..753bd7f4 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -12,6 +12,8 @@ module.exports = { "import/resolver": { alias: { map: [ + ["@task", "./task"], + ["@forks", "./test-forks"], ["@utils", "./test-utils"], ["types/generated", "./types/generated/index", "types/contracts"], ], diff --git a/.solcover.js b/.solcover.js index 29119721..d76b13fe 100644 --- a/.solcover.js +++ b/.solcover.js @@ -11,6 +11,8 @@ module.exports = { "peripheral", "savings/peripheral", "upgradability", + "legacy", + "legacy-upgraded", ], mocha: { grep: "@skip-on-coverage", // Find everything with this tag diff --git a/contracts/interfaces/IERC4626Vault.sol b/contracts/interfaces/IERC4626Vault.sol index 00c13570..7555f559 100644 --- a/contracts/interfaces/IERC4626Vault.sol +++ b/contracts/interfaces/IERC4626Vault.sol @@ -1,99 +1,100 @@ // SPDX-License-Identifier: AGPL-3.0-or-later pragma solidity 0.8.6; -import { IERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; -/// @title Yield Bearing Vault +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +/** + * @title Tokenized Vault Standard (ERC-4626) Interface + * @author mStable + * @notice See the following for the full ERC-4626 specification https://eips.ethereum.org/EIPS/eip-4626. + * @dev VERSION: 1.0 + * DATE: 2022-02-10 + */ interface IERC4626Vault { - /** - * @dev Must be an ERC-20 token contract. Must not revert. - * - * Returns the address of the underlying token used for the Vault uses for accounting, depositing, and withdrawing - */ + /// @notice The address of the underlying token used for the Vault uses for accounting, depositing, and withdrawing function asset() external view returns (address assetTokenAddress); - /** - * @dev It should include any compounding that occurs from yield. It must be inclusive of any fees that are charged against assets in the Vault. It must not revert. - * - * Returns the total amount of the underlying asset that is “managed” by Vault. - */ + /// @notice Total amount of the underlying asset that is “managed” by Vault function totalAssets() external view returns (uint256 totalManagedAssets); /** - * @dev It must be inclusive of any fees that are charged against assets in the Vault. - * - * Returns the current exchange rate of shares to assets, quoted per unit share (share unit is 10 ** Vault.decimals()). + * @notice The amount of shares that the Vault would exchange for the amount of assets provided, in an ideal scenario where all the conditions are met. + * @param assets The amount of underlying assets to be convert to vault shares. + * @return shares The amount of vault shares converted from the underlying assets. */ - function assetsPerShare() external view returns (uint256 assetsPerUnitShare); + function convertToShares(uint256 assets) external view returns (uint256 shares); /** - * @dev It MAY be more accurate than using assetsPerShare or totalAssets / Vault.totalSupply for certain types of fee calculations. - * - * Returns the total number of underlying assets that depositor’s shares represent. + * @notice The amount of assets that the Vault would exchange for the amount of shares provided, in an ideal scenario where all the conditions are met. + * @param shares The amount of vault shares to be converted to the underlying assets. + * @return assets The amount of underlying assets converted from the vault shares. */ - function assetsOf(address depositor) external view returns (uint256 assets); + function convertToAssets(uint256 shares) external view returns (uint256 assets); /** - * @dev It must return a limited value if caller is subject to some deposit limit. must return 2 ** 256 - 1 if there is no limit on the maximum amount of assets that may be deposited. - * - * Returns the total number of underlying assets that caller can be deposit. + * @notice The maximum number of underlying assets that caller can deposit. + * @param caller Account that the assets will be transferred from. + * @return maxAssets The maximum amount of underlying assets the caller can deposit. */ function maxDeposit(address caller) external view returns (uint256 maxAssets); /** - * @dev It must return the exact amount of Vault shares that would be minted if the caller were to deposit a given exact amount of underlying assets using the deposit method. - * - * It simulate the effects of their deposit at the current block, given current on-chain conditions. - * Returns the amount of shares. + * @notice Allows an on-chain or off-chain user to simulate the effects of their deposit at the current block, given current on-chain conditions. + * @param assets The amount of underlying assets to be transferred. + * @return shares The amount of vault shares that will be minted. */ function previewDeposit(uint256 assets) external view returns (uint256 shares); /** - * - * Mints shares Vault shares to receiver by depositing exactly amount of underlying tokens. - * Returns the amount of shares minted. - * Emits a {Deposit} event. + * @notice Mint vault shares to receiver by transferring exact amount of underlying asset tokens from the caller. + * @param assets The amount of underlying assets to be transferred to the vault. + * @param receiver The account that the vault shares will be minted to. + * @return shares The amount of vault shares that were minted. */ function deposit(uint256 assets, address receiver) external returns (uint256 shares); /** - * @dev must return a limited value if caller is subject to some deposit limit. must return 2 ** 256 - 1 if there is no limit on the maximum amount of shares that may be minted - * - * Returns Total number of underlying shares that caller can be mint. + * @notice The maximum number of vault shares that caller can mint. + * @param caller Account that the underlying assets will be transferred from. + * @return maxShares The maximum amount of vault shares the caller can mint. */ function maxMint(address caller) external view returns (uint256 maxShares); /** - * @dev Allows an on-chain or off-chain user to simulate the effects of their mint at the current block, given current on-chain conditions. - * - * Returns Total number of underlying shares to be minted. + * @notice Allows an on-chain or off-chain user to simulate the effects of their mint at the current block, given current on-chain conditions. + * @param shares The amount of vault shares to be minted. + * @return assets The amount of underlying assests that will be transferred from the caller. */ function previewMint(uint256 shares) external view returns (uint256 assets); /** - * Mints exactly shares Vault shares to receiver by depositing amount of underlying tokens. - * - * Returns Total number of underlying shares that caller mint. - * Emits a {Deposit} event. + * @notice Mint exact amount of vault shares to the receiver by transferring enough underlying asset tokens from the caller. + * @param shares The amount of vault shares to be minted. + * @param receiver The account the vault shares will be minted to. + * @return assets The amount of underlying assets that were transferred from the caller. */ function mint(uint256 shares, address receiver) external returns (uint256 assets); /** - * - * Returns Total number of underlying assets that caller can withdraw. + * @notice The maximum number of underlying assets that owner can withdraw. + * @param owner Account that owns the vault shares. + * @return maxAssets The maximum amount of underlying assets the owner can withdraw. */ - function maxWithdraw(address caller) external view returns (uint256 maxAssets); + function maxWithdraw(address owner) external view returns (uint256 maxAssets); /** - * @dev Allows an on-chain or off-chain user to simulate the effects of their withdrawal at the current block, given current on-chain conditions. - * - * Return the exact amount of Vault shares that would be redeemed by the caller if withdrawing a given exact amount of underlying assets using the withdraw method. + * @notice Allows an on-chain or off-chain user to simulate the effects of their withdrawal at the current block, given current on-chain conditions. + * @param assets The amount of underlying assets to be withdrawn. + * @return shares The amount of vault shares that will be burnt. */ function previewWithdraw(uint256 assets) external view returns (uint256 shares); /** - * Redeems shares from owner and sends assets of underlying tokens to receiver. - * Returns Total number of underlying shares redeemed. - * Emits a {Withdraw} event. + * @notice Burns enough vault shares from owner and transfers the exact amount of underlying asset tokens to the receiver. + * @param assets The amount of underlying assets to be withdrawn from the vault. + * @param receiver The account that the underlying assets will be transferred to. + * @param owner Account that owns the vault shares to be burnt. + * @return shares The amount of vault shares that were burnt. */ function withdraw( uint256 assets, @@ -102,24 +103,25 @@ interface IERC4626Vault { ) external returns (uint256 shares); /** - * @dev it must return a limited value if caller is subject to some withdrawal limit or timelock. must return balanceOf(caller) if caller is not subject to any withdrawal limit or timelock. MAY be used in the previewRedeem or redeem methods for shares input parameter. must NOT revert. - * - * Returns Total number of underlying shares that caller can redeem. + * @notice The maximum number of shares an owner can redeem for underlying assets. + * @param owner Account that owns the vault shares. + * @return maxShares The maximum amount of shares the owner can redeem. */ - function maxRedeem(address caller) external view returns (uint256); + function maxRedeem(address owner) external view returns (uint256 maxShares); /** - * @dev Allows an on-chain or off-chain user to simulate the effects of their redeemption at the current block, given current on-chain conditions. - * - * Returns the exact amount of underlying assets that would be withdrawn by the caller if redeeming a given exact amount of Vault shares using the redeem method + * @notice Allows an on-chain or off-chain user to simulate the effects of their redeemption at the current block, given current on-chain conditions. + * @param shares The amount of vault shares to be burnt. + * @return assets The amount of underlying assests that will transferred to the receiver. */ function previewRedeem(uint256 shares) external view returns (uint256 assets); /** - * Redeems shares from owner and sends assets of underlying tokens to receiver. - * - * Returns Total number of underlying assets of underlying redeemed. - * Emits a {Withdraw} event. + * @notice Burns exact amount of vault shares from owner and transfers the underlying asset tokens to the receiver. + * @param shares The amount of vault shares to be burnt. + * @param receiver The account the underlying assets will be transferred to. + * @param owner The account that owns the vault shares to be burnt. + * @return assets The amount of underlying assets that were transferred to the receiver. */ function redeem( uint256 shares, @@ -132,12 +134,12 @@ interface IERC4626Vault { //////////////////////////////////////////////////////////////*/ /** - * @dev Emitted when sender has exchanged assets for shares, and transferred those shares to receiver. + * @dev Emitted when caller has exchanged assets for shares, and transferred those shares to owner. * * Note It must be emitted when tokens are deposited into the Vault in ERC4626.mint or ERC4626.deposit methods. * */ - event Deposit(address indexed sender, address indexed receiver, uint256 assets); + event Deposit(address indexed caller, address indexed owner, uint256 assets, uint256 shares); /** * @dev Emitted when sender has exchanged shares for assets, and transferred those assets to receiver. * @@ -145,8 +147,9 @@ interface IERC4626Vault { * */ event Withdraw( - address indexed sender, + address indexed caller, address indexed receiver, + address indexed owner, uint256 assets, uint256 shares ); diff --git a/contracts/interfaces/ISavingsContract.sol b/contracts/interfaces/ISavingsContract.sol index 417c2745..77bd50c0 100644 --- a/contracts/interfaces/ISavingsContract.sol +++ b/contracts/interfaces/ISavingsContract.sol @@ -46,36 +46,27 @@ interface ISavingsContractV2 { function underlying() external view returns (IERC20 underlyingMasset); // V2 } -interface ISavingsContractV3 is IERC4626Vault { +interface ISavingsContractV3 { // DEPRECATED but still backwards compatible function redeem(uint256 _amount) external returns (uint256 massetReturned); function creditBalances(address) external view returns (uint256); // V1 & V2 (use balanceOf) - /*/////////////////////////////////////////////////////////////// - DEPRECATED for IERC4626Vault - //////////////////////////////////////////////////////////////*/ - /** @dev Deprecated in favour of IERC4626Vault.deposit(uint256 assets, address receiver)*/ + // -------------------------------------------- + function depositInterest(uint256 _amount) external; // V1 & V2 + + /** @dev see IERC4626Vault.deposit(uint256 assets, address receiver)*/ function depositSavings(uint256 _amount) external returns (uint256 creditsIssued); // V1 & V2 + /** @dev see IERC4626Vault.deposit(uint256 assets, address receiver)*/ function depositSavings(uint256 _amount, address _beneficiary) external returns (uint256 creditsIssued); // V2 + /** @dev see IERC4626Vault.redeem(uint256 shares,address receiver,address owner)(uint256 assets);*/ function redeemCredits(uint256 _amount) external returns (uint256 underlyingReturned); // V2 - /** @dev Deprecated in favour of IERC4626Vault.withdraw(uint256 assets,address receiver,address owner)(uint256 assets, address receiver)*/ - function redeemUnderlying(uint256 _amount) external returns (uint256 creditsBurned); // V2 - - /** @dev Deprecated in favour of IERC4626Vault.assetsPerShare() external view returns (uint256 assetsPerUnitShare);*/ - function exchangeRate() external view returns (uint256); // V1 & V2 - - /** @dev Deprecated in favour of IERC4626Vault.assetsOf(addresss depositor) view returns (uint256 assets)*/ - function balanceOfUnderlying(address _user) external view returns (uint256 underlying); // V2 - - /** @dev Deprecated in favour of IERC4626Vault.asset()(address assetTokenAddress);*/ - function underlying() external view returns (IERC20 underlyingMasset); // V2 - + /** @dev see IERC4626Vault.withdraw(uint256 assets,address receiver,address owner)(uint256 assets, address receiver)*/ function redeemUnderlying(uint256 _amount) external returns (uint256 creditsBurned); // V2 function exchangeRate() external view returns (uint256); // V1 & V2 @@ -86,14 +77,10 @@ interface ISavingsContractV3 is IERC4626Vault { function creditsToUnderlying(uint256 _credits) external view returns (uint256 underlying); // V2 + /** @dev see IERC4626Vault.asset()(address assetTokenAddress);*/ function underlying() external view returns (IERC20 underlyingMasset); // V2 // -------------------------------------------- - function deposit( - uint256 assets, - address receiver, - address referrer - ) external returns (uint256 creditsIssued); // V3 function redeemAndUnwrap( uint256 _amount, @@ -109,11 +96,11 @@ interface ISavingsContractV3 is IERC4626Vault { uint256 creditsBurned, uint256 massetRedeemed, uint256 outputQuantity - ); + ); // V3 function depositSavings( uint256 _underlying, address _beneficiary, address _referrer - ) external returns (uint256 creditsIssued); + ) external returns (uint256 creditsIssued); // V3 } diff --git a/contracts/legacy-upgraded/imbtc-mainnet-22.sol b/contracts/legacy-upgraded/imbtc-mainnet-22.sol new file mode 100644 index 00000000..80f84b7d --- /dev/null +++ b/contracts/legacy-upgraded/imbtc-mainnet-22.sol @@ -0,0 +1,1681 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later +pragma solidity ^0.8.0; + +interface IUnwrapper { + // @dev Get bAssetOut status + function getIsBassetOut( + address _masset, + bool _inputIsCredit, + address _output + ) external view returns (bool isBassetOut); + + /// @dev Estimate output + function getUnwrapOutput( + bool _isBassetOut, + address _router, + address _input, + bool _inputIsCredit, + address _output, + uint256 _amount + ) external view returns (uint256 output); + + /// @dev Unwrap and send + function unwrapAndSend( + bool _isBassetOut, + address _router, + address _input, + address _output, + uint256 _amount, + uint256 _minAmountOut, + address _beneficiary + ) external returns (uint256 outputQuantity); +} + +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; +} + +interface ISavingsContractV3 { + // 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 balance); // V2 + + function underlyingToCredits(uint256 _credits) external view returns (uint256 underlying); // V2 + + function creditsToUnderlying(uint256 _underlying) external view returns (uint256 credits); // V2 + + // -------------------------------------------- + + function redeemAndUnwrap( + uint256 _amount, + bool _isCreditAmt, + uint256 _minAmountOut, + address _output, + address _beneficiary, + address _router, + bool _isBassetOut + ) + external + returns ( + uint256 creditsBurned, + uint256 massetRedeemed, + uint256 outputQuantity + ); + + function depositSavings( + uint256 _underlying, + address _beneficiary, + address _referrer + ) external returns (uint256 creditsIssued); +} + +/* + * @dev Provides information about the current execution context, including the + * sender of the transaction and its data. While these are generally available + * via msg.sender and msg.data, they should not be accessed in such a direct + * manner, since when dealing with GSN meta-transactions the account sending and + * paying for execution may not be the actual sender (as far as an application + * is concerned). + * + * This contract is only required for intermediate, library-like contracts. + */ +abstract contract Context { + // Empty internal constructor, to prevent people from mistakenly deploying + // an instance of this contract, which should be used via inheritance. + // constructor () internal { } + // solhint-disable-previous-line no-empty-blocks + + function _msgSender() internal view returns (address payable) { + return payable(msg.sender); + } + + function _msgData() internal view returns (bytes memory) { + this; // silence state mutability warning without generating bytecode - see https://github.com/ethereum/solidity/issues/2691 + return msg.data; + } +} + +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); +} + +contract ERC205 is Context, IERC20 { + mapping(address => uint256) private _balances; + + mapping(address => mapping(address => uint256)) private _allowances; + + uint256 private _totalSupply; + + /** + * @dev See {IERC20-totalSupply}. + */ + function totalSupply() public view override returns (uint256) { + return _totalSupply; + } + + /** + * @dev See {IERC20-balanceOf}. + */ + function balanceOf(address account) public view override returns (uint256) { + return _balances[account]; + } + + /** + * @dev See {IERC20-transfer}. + * + * Requirements: + * + * - `recipient` cannot be the zero address. + * - the caller must have a balance of at least `amount`. + */ + function transfer(address recipient, uint256 amount) public override returns (bool) { + _transfer(_msgSender(), recipient, amount); + return true; + } + + /** + * @dev See {IERC20-allowance}. + */ + function allowance(address owner, address spender) public view override returns (uint256) { + return _allowances[owner][spender]; + } + + /** + * @dev See {IERC20-approve}. + * + * Requirements: + * + * - `spender` cannot be the zero address. + */ + function approve(address spender, uint256 amount) public override returns (bool) { + _approve(_msgSender(), spender, amount); + return true; + } + + /** + * @dev See {IERC20-transferFrom}. + * + * Emits an {Approval} event indicating the updated allowance. This is not + * required by the EIP. See the note at the beginning of {ERC20}; + * + * Requirements: + * - `sender` and `recipient` cannot be the zero address. + * - `sender` must have a balance of at least `amount`. + * - the caller must have allowance for `sender`'s tokens of at least + * `amount`. + */ + function transferFrom( + address sender, + address recipient, + uint256 amount + ) public override returns (bool) { + _transfer(sender, recipient, amount); + _approve(sender, _msgSender(), _allowances[sender][_msgSender()] - amount); + return true; + } + + /** + * @dev Atomically increases the allowance granted to `spender` by the caller. + * + * This is an alternative to {approve} that can be used as a mitigation for + * problems described in {IERC20-approve}. + * + * Emits an {Approval} event indicating the updated allowance. + * + * Requirements: + * + * - `spender` cannot be the zero address. + */ + function increaseAllowance(address spender, uint256 addedValue) public returns (bool) { + _approve(_msgSender(), spender, _allowances[_msgSender()][spender] + addedValue); + return true; + } + + /** + * @dev Atomically decreases the allowance granted to `spender` by the caller. + * + * This is an alternative to {approve} that can be used as a mitigation for + * problems described in {IERC20-approve}. + * + * Emits an {Approval} event indicating the updated allowance. + * + * Requirements: + * + * - `spender` cannot be the zero address. + * - `spender` must have allowance for the caller of at least + * `subtractedValue`. + */ + function decreaseAllowance(address spender, uint256 subtractedValue) public returns (bool) { + _approve(_msgSender(), spender, _allowances[_msgSender()][spender] - subtractedValue); + return true; + } + + /** + * @dev Moves tokens `amount` from `sender` to `recipient`. + * + * This is internal function is equivalent to {transfer}, and can be used to + * e.g. implement automatic token fees, slashing mechanisms, etc. + * + * Emits a {Transfer} event. + * + * Requirements: + * + * - `sender` cannot be the zero address. + * - `recipient` cannot be the zero address. + * - `sender` must have a balance of at least `amount`. + */ + function _transfer( + address sender, + address recipient, + uint256 amount + ) internal { + require(sender != address(0), "ERC20: transfer from the zero address"); + require(recipient != address(0), "ERC20: transfer to the zero address"); + + _balances[sender] -= amount; + _balances[recipient] += amount; + emit Transfer(sender, recipient, amount); + } + + /** @dev Creates `amount` tokens and assigns them to `account`, increasing + * the total supply. + * + * Emits a {Transfer} event with `from` set to the zero address. + * + * Requirements + * + * - `to` cannot be the zero address. + */ + function _mint(address account, uint256 amount) internal { + require(account != address(0), "ERC20: mint to the zero address"); + + _totalSupply += amount; + _balances[account] += amount; + emit Transfer(address(0), account, amount); + } + + /** + * @dev Destroys `amount` tokens from `account`, reducing the + * total supply. + * + * Emits a {Transfer} event with `to` set to the zero address. + * + * Requirements + * + * - `account` cannot be the zero address. + * - `account` must have at least `amount` tokens. + */ + function _burn(address account, uint256 amount) internal { + require(account != address(0), "ERC20: burn from the zero address"); + + _balances[account] -= amount; + _totalSupply -= amount; + emit Transfer(account, address(0), amount); + } + + /** + * @dev Sets `amount` as the allowance of `spender` over the `owner`s tokens. + * + * This is internal function is equivalent to `approve`, and can be used to + * e.g. set automatic allowances for certain subsystems, etc. + * + * Emits an {Approval} event. + * + * Requirements: + * + * - `owner` cannot be the zero address. + * - `spender` cannot be the zero address. + */ + function _approve( + address owner, + address spender, + uint256 amount + ) internal { + require(owner != address(0), "ERC20: approve from the zero address"); + require(spender != address(0), "ERC20: approve to the zero address"); + + _allowances[owner][spender] = amount; + emit Approval(owner, spender, amount); + } + + /** + * @dev Destroys `amount` tokens from `account`.`amount` is then deducted + * from the caller's allowance. + * + * See {_burn} and {_approve}. + */ + function _burnFrom(address account, uint256 amount) internal { + _burn(account, amount); + _approve(account, _msgSender(), _allowances[account][_msgSender()] - amount); + } +} + +abstract contract InitializableERC20Detailed is IERC20 { + string private _name; + string private _symbol; + uint8 private _decimals; + + /** + * @dev Sets the values for `name`, `symbol`, and `decimals`. All three of + * these values are immutable: they can only be set once during + * construction. + * @notice To avoid variable shadowing appended `Arg` after arguments name. + */ + function _initialize( + string memory nameArg, + string memory symbolArg, + uint8 decimalsArg + ) internal { + _name = nameArg; + _symbol = symbolArg; + _decimals = decimalsArg; + } + + /** + * @dev Returns the name of the token. + */ + function name() public view returns (string memory) { + return _name; + } + + /** + * @dev Returns the symbol of the token, usually a shorter version of the + * name. + */ + function symbol() public view returns (string memory) { + return _symbol; + } + + /** + * @dev Returns the number of decimals used to get its user representation. + * For example, if `decimals` equals `2`, a balance of `505` tokens should + * be displayed to a user as `5,05` (`505 / 10 ** 2`). + * + * Tokens usually opt for a value of 18, imitating the relationship between + * Ether and Wei. + * + * NOTE: This information is only used for _display_ purposes: it in + * no way affects any of the arithmetic of the contract, including + * {IERC20-balanceOf} and {IERC20-transfer}. + */ + function decimals() public view returns (uint8) { + return _decimals; + } +} + +abstract contract InitializableToken is ERC205, InitializableERC20Detailed { + /** + * @dev Initialization function for implementing contract + * @notice To avoid variable shadowing appended `Arg` after arguments name. + */ + function _initialize(string memory _nameArg, string memory _symbolArg) internal { + InitializableERC20Detailed._initialize(_nameArg, _symbolArg, 18); + } +} + +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; +} + +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 Modifier to allow function calls only from the ProxyAdmin. + */ + modifier onlyProxyAdmin() { + require(msg.sender == _proxyAdmin(), "Only ProxyAdmin can execute"); + _; + } + + /** + * @dev Modifier to allow function calls only from the Manager. + */ + modifier onlyManager() { + require(msg.sender == _manager(), "Only manager 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 Staking Module address from the Nexus + * @return Address of the Staking Module contract + */ + function _staking() internal view returns (address) { + return nexus.getModule(KEY_STAKING); + } + + /** + * @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); + } + + /** + * @dev Return MetaToken Module address from the Nexus + * @return Address of the MetaToken Module contract + */ + function _metaToken() internal view returns (address) { + return nexus.getModule(KEY_META_TOKEN); + } + + /** + * @dev Return OracleHub Module address from the Nexus + * @return Address of the OracleHub Module contract + */ + function _oracleHub() internal view returns (address) { + return nexus.getModule(KEY_ORACLE_HUB); + } + + /** + * @dev Return Manager Module address from the Nexus + * @return Address of the Manager Module contract + */ + function _manager() internal view returns (address) { + return nexus.getModule(KEY_MANAGER); + } + + /** + * @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); + } +} + +interface IConnector { + /** + * @notice Deposits the mAsset into the connector + * @param _amount Units of mAsset to receive and deposit + */ + function deposit(uint256 _amount) external; + + /** + * @notice Withdraws a specific amount of mAsset from the connector + * @param _amount Units of mAsset to withdraw + */ + function withdraw(uint256 _amount) external; + + /** + * @notice Withdraws all mAsset from the connector + */ + function withdrawAll() external; + + /** + * @notice Returns the available balance in the connector. In connections + * where there is likely to be an initial dip in value due to conservative + * exchange rates (e.g. with Curves `get_virtual_price`), it should return + * max(deposited, balance) to avoid temporary negative yield. Any negative yield + * should be corrected during a withdrawal or over time. + * @return Balance of mAsset in the connector + */ + function checkBalance() external view returns (uint256); +} + +contract Initializable { + /** + * @dev Indicates that the contract has been initialized. + */ + bool private initialized; + + /** + * @dev Indicates that the contract is in the process of being initialized. + */ + bool private initializing; + + /** + * @dev Modifier to use in the initializer function of a contract. + */ + modifier initializer() { + require( + initializing || isConstructor() || !initialized, + "Contract instance has already been initialized" + ); + + bool isTopLevelCall = !initializing; + if (isTopLevelCall) { + initializing = true; + initialized = true; + } + + _; + + if (isTopLevelCall) { + initializing = false; + } + } + + /// @dev Returns true if and only if the function is running in the constructor + function isConstructor() private view returns (bool) { + // extcodesize checks the size of the code stored in an address, and + // address returns the current address. Since the code is still not + // deployed when running a constructor, any checks on its code size will + // yield zero, making it an effective way to detect if a contract is + // under construction or not. + address self = address(this); + uint256 cs; + assembly { + cs := extcodesize(self) + } + return cs == 0; + } + + // Reserved storage space to allow for layout changes in the future. + uint256[50] private ______gap; +} + +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 9e38 / 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; + } +} + +/** + * @title SavingsContract + * @author mStable + * @notice Savings contract uses the ever increasing "exchangeRate" to increase + * the value of the Savers "credits" (ERC20) relative to the amount of additional + * underlying collateral that has been deposited into this contract ("interest") + * @dev VERSION: 2.1 + * DATE: 2021-11-25 + */ +contract SavingsContract_imbtc_mainnet_22 is + ISavingsContractV3, + Initializable, + InitializableToken, + ImmutableModule +{ + using StableMath for uint256; + + // Core events for depositing and withdrawing + event ExchangeRateUpdated(uint256 newExchangeRate, uint256 interestCollected); + event SavingsDeposited(address indexed saver, uint256 savingsDeposited, uint256 creditsIssued); + event CreditsRedeemed( + address indexed redeemer, + uint256 creditsRedeemed, + uint256 savingsCredited + ); + + event AutomaticInterestCollectionSwitched(bool automationEnabled); + + // Connector poking + event PokerUpdated(address poker); + + event FractionUpdated(uint256 fraction); + event ConnectorUpdated(address connector); + event EmergencyUpdate(); + + event Poked(uint256 oldBalance, uint256 newBalance, uint256 interestDetected); + event PokedRaw(); + + // Tracking events + event Referral(address indexed referrer, address beneficiary, uint256 amount); + + // Rate between 'savings credits' and underlying + // e.g. 1 credit (1e17) mulTruncate(exchangeRate) = underlying, starts at 10:1 + // exchangeRate increases over time + uint256 private constant startingRate = 1e17; + uint256 public override exchangeRate; + + // Underlying asset is underlying + IERC20 public immutable underlying; + bool private automateInterestCollection; + + // Yield + // Poker is responsible for depositing/withdrawing from connector + address public poker; + // Last time a poke was made + uint256 public lastPoke; + // Last known balance of the connector + uint256 public lastBalance; + // Fraction of capital assigned to the connector (100% = 1e18) + uint256 public fraction; + // Address of the current connector (all IConnectors are mStable validated) + IConnector public connector; + // How often do we allow pokes + uint256 private constant POKE_CADENCE = 4 hours; + // Max APY generated on the capital in the connector + uint256 private constant MAX_APY = 4e18; + uint256 private constant SECONDS_IN_YEAR = 365 days; + // Proxy contract for easy redemption + address public immutable unwrapper; + + constructor( + address _nexus, + address _underlying, + address _unwrapper + ) ImmutableModule(_nexus) { + require(_underlying != address(0), "mAsset address is zero"); + require(_unwrapper != address(0), "Unwrapper address is zero"); + underlying = IERC20(_underlying); + unwrapper = _unwrapper; + } + + // Add these constants to bytecode at deploytime + function initialize( + address _poker, + string calldata _nameArg, + string calldata _symbolArg + ) external initializer { + InitializableToken._initialize(_nameArg, _symbolArg); + + require(_poker != address(0), "Invalid poker address"); + poker = _poker; + + fraction = 2e17; + automateInterestCollection = true; + exchangeRate = startingRate; + } + + /** @dev Only the savings managaer (pulled from Nexus) can execute this */ + modifier onlySavingsManager() { + require(msg.sender == _savingsManager(), "Only savings manager can execute"); + _; + } + + /*************************************** + VIEW - E + ****************************************/ + + /** + * @dev Returns the underlying balance of a given user + * @param _user Address of the user to check + * @return balance Units of underlying owned by the user + */ + function balanceOfUnderlying(address _user) external view override returns (uint256 balance) { + (balance, ) = _creditsToUnderlying(balanceOf(_user)); + } + + /** + * @dev Converts a given underlying amount into credits + * @param _underlying Units of underlying + * @return credits Credit units (a.k.a imUSD) + */ + function underlyingToCredits(uint256 _underlying) + external + view + override + returns (uint256 credits) + { + (credits, ) = _underlyingToCredits(_underlying); + } + + /** + * @dev Converts a given credit amount into underlying + * @param _credits Units of credits + * @return amount Corresponding underlying amount + */ + function creditsToUnderlying(uint256 _credits) external view override returns (uint256 amount) { + (amount, ) = _creditsToUnderlying(_credits); + } + + // Deprecated in favour of `balanceOf(address)` + // Maintained for backwards compatibility + // Returns the credit balance of a given user + function creditBalances(address _user) external view override returns (uint256) { + return balanceOf(_user); + } + + /*************************************** + INTEREST + ****************************************/ + + /** + * @dev Deposit interest (add to savings) and update exchange rate of contract. + * Exchange rate is calculated as the ratio between new savings q and credits: + * exchange rate = savings / credits + * + * @param _amount Units of underlying to add to the savings vault + */ + function depositInterest(uint256 _amount) external override onlySavingsManager { + require(_amount > 0, "Must deposit something"); + + // Transfer the interest from sender to here + require(underlying.transferFrom(msg.sender, address(this), _amount), "Must receive tokens"); + + // Calc new exchange rate, protect against initialisation case + uint256 totalCredits = totalSupply(); + if (totalCredits > 0) { + // new exchange rate is relationship between _totalCredits & totalSavings + // _totalCredits * exchangeRate = totalSavings + // exchangeRate = totalSavings/_totalCredits + (uint256 totalCollat, ) = _creditsToUnderlying(totalCredits); + uint256 newExchangeRate = _calcExchangeRate(totalCollat + _amount, totalCredits); + exchangeRate = newExchangeRate; + + emit ExchangeRateUpdated(newExchangeRate, _amount); + } + } + + /** @dev Enable or disable the automation of fee collection during deposit process */ + function automateInterestCollectionFlag(bool _enabled) external onlyGovernor { + automateInterestCollection = _enabled; + emit AutomaticInterestCollectionSwitched(_enabled); + } + + /*************************************** + DEPOSIT + ****************************************/ + + /** + * @dev During a migration period, allow savers to deposit underlying here before the interest has been redirected + * @param _underlying Units of underlying to deposit into savings vault + * @param _beneficiary Immediately transfer the imUSD token to this beneficiary address + * @return creditsIssued Units of credits (imUSD) issued + */ + function preDeposit(uint256 _underlying, address _beneficiary) + external + returns (uint256 creditsIssued) + { + require(exchangeRate == startingRate, "Can only use this method before streaming begins"); + return _deposit(_underlying, _beneficiary, false); + } + + /** + * @dev Deposit the senders savings to the vault, and credit them internally with "credits". + * Credit amount is calculated as a ratio of deposit amount and exchange rate: + * credits = underlying / exchangeRate + * We will first update the internal exchange rate by collecting any interest generated on the underlying. + * @param _underlying Units of underlying to deposit into savings vault + * @return creditsIssued Units of credits (imUSD) issued + */ + function depositSavings(uint256 _underlying) external override returns (uint256 creditsIssued) { + return _deposit(_underlying, msg.sender, true); + } + + /** + * @dev Deposit the senders savings to the vault, and credit them internally with "credits". + * Credit amount is calculated as a ratio of deposit amount and exchange rate: + * credits = underlying / exchangeRate + * We will first update the internal exchange rate by collecting any interest generated on the underlying. + * @param _underlying Units of underlying to deposit into savings vault + * @param _beneficiary Immediately transfer the imUSD token to this beneficiary address + * @return creditsIssued Units of credits (imUSD) issued + */ + function depositSavings(uint256 _underlying, address _beneficiary) + external + override + returns (uint256 creditsIssued) + { + return _deposit(_underlying, _beneficiary, true); + } + + /** + * @dev Overloaded `depositSavings` method with an optional referrer address. + * @param _underlying Units of underlying to deposit into savings vault + * @param _beneficiary Immediately transfer the imUSD token to this beneficiary address + * @param _referrer Referrer address for this deposit + * @return creditsIssued Units of credits (imUSD) issued + */ + function depositSavings( + uint256 _underlying, + address _beneficiary, + address _referrer + ) external override returns (uint256 creditsIssued) { + emit Referral(_referrer, _beneficiary, _underlying); + return _deposit(_underlying, _beneficiary, true); + } + + /** + * @dev Internally deposit the _underlying from the sender and credit the beneficiary with new imUSD + */ + function _deposit( + uint256 _underlying, + address _beneficiary, + bool _collectInterest + ) internal returns (uint256 creditsIssued) { + require(_underlying > 0, "Must deposit something"); + require(_beneficiary != address(0), "Invalid beneficiary address"); + + // Collect recent interest generated by basket and update exchange rate + IERC20 mAsset = underlying; + if (_collectInterest) { + ISavingsManager(_savingsManager()).collectAndDistributeInterest(address(mAsset)); + } + + // Transfer tokens from sender to here + require(mAsset.transferFrom(msg.sender, address(this), _underlying), "Must receive tokens"); + + // Calc how many credits they receive based on currentRatio + (creditsIssued, ) = _underlyingToCredits(_underlying); + + // add credits to ERC20 balances + _mint(_beneficiary, creditsIssued); + + emit SavingsDeposited(_beneficiary, _underlying, creditsIssued); + } + + /*************************************** + REDEEM + ****************************************/ + + // Deprecated in favour of redeemCredits + // Maintaining backwards compatibility, this fn minimics the old redeem fn, in which + // credits are redeemed but the interest from the underlying is not collected. + function redeem(uint256 _credits) external override returns (uint256 massetReturned) { + require(_credits > 0, "Must withdraw something"); + + (, uint256 payout) = _redeem(_credits, true, true); + + // Collect recent interest generated by basket and update exchange rate + if (automateInterestCollection) { + ISavingsManager(_savingsManager()).collectAndDistributeInterest(address(underlying)); + } + + return payout; + } + + /** + * @dev Redeem specific number of the senders "credits" in exchange for underlying. + * Payout amount is calculated as a ratio of credits and exchange rate: + * payout = credits * exchangeRate + * @param _credits Amount of credits to redeem + * @return massetReturned Units of underlying mAsset paid out + */ + function redeemCredits(uint256 _credits) external override returns (uint256 massetReturned) { + require(_credits > 0, "Must withdraw something"); + + // Collect recent interest generated by basket and update exchange rate + if (automateInterestCollection) { + ISavingsManager(_savingsManager()).collectAndDistributeInterest(address(underlying)); + } + + (, uint256 payout) = _redeem(_credits, true, true); + + return payout; + } + + /** + * @dev Redeem credits into a specific amount of underlying. + * Credits needed to burn is calculated using: + * credits = underlying / exchangeRate + * @param _underlying Amount of underlying to redeem + * @return creditsBurned Units of credits burned from sender + */ + function redeemUnderlying(uint256 _underlying) + external + override + returns (uint256 creditsBurned) + { + require(_underlying > 0, "Must withdraw something"); + + // Collect recent interest generated by basket and update exchange rate + if (automateInterestCollection) { + ISavingsManager(_savingsManager()).collectAndDistributeInterest(address(underlying)); + } + + // Ensure that the payout was sufficient + (uint256 credits, uint256 massetReturned) = _redeem(_underlying, false, true); + require(massetReturned == _underlying, "Invalid output"); + + return credits; + } + + /** + * @notice Redeem credits into a specific amount of underlying, unwrap + * into a selected output asset, and send to a beneficiary + * Credits needed to burn is calculated using: + * credits = underlying / exchangeRate + * @param _amount Units to redeem (either underlying or credit amount). + * @param _isCreditAmt `true` if `amount` is in credits. eg imUSD. `false` if `amount` is in underlying. eg mUSD. + * @param _minAmountOut Minimum amount of `output` tokens to unwrap for. This is to the same decimal places as the `output` token. + * @param _output Asset to receive in exchange for the redeemed mAssets. This can be a bAsset or a fAsset. For example: + - bAssets (USDC, DAI, sUSD or USDT) or fAssets (GUSD, BUSD, alUSD, FEI or RAI) for mainnet imUSD Vault. + - bAssets (USDC, DAI or USDT) or fAsset FRAX for Polygon imUSD Vault. + - bAssets (WBTC, sBTC or renBTC) or fAssets (HBTC or TBTCV2) for mainnet imBTC Vault. + * @param _beneficiary Address to send `output` tokens to. + * @param _router mAsset address if the output is a bAsset. Feeder Pool address if the output is a fAsset. + * @param _isBassetOut `true` if `output` is a bAsset. `false` if `output` is a fAsset. + * @return creditsBurned Units of credits burned from sender. eg imUSD or imBTC. + * @return massetReturned Units of the underlying mAssets that were redeemed or swapped for the output tokens. eg mUSD or mBTC. + * @return outputQuantity Units of `output` tokens sent to the beneficiary. + */ + function redeemAndUnwrap( + uint256 _amount, + bool _isCreditAmt, + uint256 _minAmountOut, + address _output, + address _beneficiary, + address _router, + bool _isBassetOut + ) + external + override + returns ( + uint256 creditsBurned, + uint256 massetReturned, + uint256 outputQuantity + ) + { + require(_amount > 0, "Must withdraw something"); + require(_output != address(0), "Output address is zero"); + require(_beneficiary != address(0), "Beneficiary address is zero"); + require(_router != address(0), "Router address is zero"); + + // Collect recent interest generated by basket and update exchange rate + if (automateInterestCollection) { + ISavingsManager(_savingsManager()).collectAndDistributeInterest(address(underlying)); + } + + // Ensure that the payout was sufficient + (creditsBurned, massetReturned) = _redeem(_amount, _isCreditAmt, false); + require( + _isCreditAmt ? creditsBurned == _amount : massetReturned == _amount, + "Invalid output" + ); + + // Approve wrapper to spend contract's underlying; just for this tx + underlying.approve(unwrapper, massetReturned); + + // Unwrap the underlying into `output` and transfer to `beneficiary` + outputQuantity = IUnwrapper(unwrapper).unwrapAndSend( + _isBassetOut, + _router, + address(underlying), + _output, + massetReturned, + _minAmountOut, + _beneficiary + ); + } + + /** + * @dev Internally burn the credits and send the underlying to msg.sender + */ + function _redeem( + uint256 _amt, + bool _isCreditAmt, + bool _transferUnderlying + ) internal returns (uint256 creditsBurned, uint256 massetReturned) { + // Centralise credit <> underlying calcs and minimise SLOAD count + uint256 credits_; + uint256 underlying_; + uint256 exchangeRate_; + // If the input is a credit amt, then calculate underlying payout and cache the exchangeRate + if (_isCreditAmt) { + credits_ = _amt; + (underlying_, exchangeRate_) = _creditsToUnderlying(_amt); + } + // If the input is in underlying, then calculate credits needed to burn + else { + underlying_ = _amt; + (credits_, exchangeRate_) = _underlyingToCredits(_amt); + } + + // Burn required credits from the sender FIRST + _burn(msg.sender, credits_); + // Optionally, transfer tokens from here to sender + if (_transferUnderlying) { + require(underlying.transfer(msg.sender, underlying_), "Must send tokens"); + } + // If this withdrawal pushes the portion of stored collateral in the `connector` over a certain + // threshold (fraction + 20%), then this should trigger a _poke on the connector. This is to avoid + // a situation in which there is a rush on withdrawals for some reason, causing the connector + // balance to go up and thus having too large an exposure. + CachedData memory cachedData = _cacheData(); + ConnectorStatus memory status = _getConnectorStatus(cachedData, exchangeRate_); + if (status.inConnector > status.limit) { + _poke(cachedData, false); + } + + emit CreditsRedeemed(msg.sender, credits_, underlying_); + + return (credits_, underlying_); + } + + struct ConnectorStatus { + // Limit is the max amount of units allowed in the connector + uint256 limit; + // Derived balance of the connector + uint256 inConnector; + } + + /** + * @dev Derives the units of collateral held in the connector + * @param _data Struct containing data on balances + * @param _exchangeRate Current system exchange rate + * @return status Contains max amount of assets allowed in connector + */ + function _getConnectorStatus(CachedData memory _data, uint256 _exchangeRate) + internal + pure + returns (ConnectorStatus memory) + { + // Total units of underlying collateralised + uint256 totalCollat = _data.totalCredits.mulTruncate(_exchangeRate); + // Max amount of underlying that can be held in the connector + uint256 limit = totalCollat.mulTruncate(_data.fraction + 2e17); + // Derives amount of underlying present in the connector + uint256 inConnector = _data.rawBalance >= totalCollat ? 0 : totalCollat - _data.rawBalance; + + return ConnectorStatus(limit, inConnector); + } + + /*************************************** + YIELD - E + ****************************************/ + + /** @dev Modifier allowing only the designated poker to execute the fn */ + modifier onlyPoker() { + require(msg.sender == poker, "Only poker can execute"); + _; + } + + /** + * @dev External poke function allows for the redistribution of collateral between here and the + * current connector, setting the ratio back to the defined optimal. + */ + function poke() external onlyPoker { + CachedData memory cachedData = _cacheData(); + _poke(cachedData, false); + } + + /** + * @dev Governance action to set the address of a new poker + * @param _newPoker Address of the new poker + */ + function setPoker(address _newPoker) external onlyGovernor { + require(_newPoker != address(0) && _newPoker != poker, "Invalid poker"); + + poker = _newPoker; + + emit PokerUpdated(_newPoker); + } + + /** + * @dev Governance action to set the percentage of assets that should be held + * in the connector. + * @param _fraction Percentage of assets that should be held there (where 20% == 2e17) + */ + function setFraction(uint256 _fraction) external onlyGovernor { + require(_fraction <= 5e17, "Fraction must be <= 50%"); + + fraction = _fraction; + + CachedData memory cachedData = _cacheData(); + _poke(cachedData, true); + + emit FractionUpdated(_fraction); + } + + /** + * @dev Governance action to set the address of a new connector, and move funds (if any) across. + * @param _newConnector Address of the new connector + */ + function setConnector(address _newConnector) external onlyGovernor { + // Withdraw all from previous by setting target = 0 + CachedData memory cachedData = _cacheData(); + cachedData.fraction = 0; + _poke(cachedData, true); + + // Set new connector + CachedData memory cachedDataNew = _cacheData(); + connector = IConnector(_newConnector); + _poke(cachedDataNew, true); + + emit ConnectorUpdated(_newConnector); + } + + /** + * @dev Governance action to perform an emergency withdraw of the assets in the connector, + * should it be the case that some or all of the liquidity is trapped in. This causes the total + * collateral in the system to go down, causing a hard refresh. + */ + function emergencyWithdraw(uint256 _withdrawAmount) external onlyGovernor { + // withdraw _withdrawAmount from connection + connector.withdraw(_withdrawAmount); + + // reset the connector + connector = IConnector(address(0)); + emit ConnectorUpdated(address(0)); + + // set fraction to 0 + fraction = 0; + emit FractionUpdated(0); + + // check total collateralisation of credits + CachedData memory data = _cacheData(); + // use rawBalance as the remaining liquidity in the connector is now written off + _refreshExchangeRate(data.rawBalance, data.totalCredits, true); + + emit EmergencyUpdate(); + } + + /*************************************** + YIELD - I + ****************************************/ + + /** @dev Internal poke function to keep the balance between connector and raw balance healthy */ + function _poke(CachedData memory _data, bool _ignoreCadence) internal { + require(_data.totalCredits > 0, "Must have something to poke"); + + // 1. Verify that poke cadence is valid, unless this is a manual action by governance + uint256 currentTime = uint256(block.timestamp); + uint256 timeSinceLastPoke = currentTime - lastPoke; + require(_ignoreCadence || timeSinceLastPoke > POKE_CADENCE, "Not enough time elapsed"); + lastPoke = currentTime; + + // If there is a connector, check the balance and settle to the specified fraction % + IConnector connector_ = connector; + if (address(connector_) != address(0)) { + // 2. Check and verify new connector balance + uint256 lastBalance_ = lastBalance; + uint256 connectorBalance = connector_.checkBalance(); + // Always expect the collateral in the connector to increase in value + require(connectorBalance >= lastBalance_, "Invalid yield"); + if (connectorBalance > 0) { + // Validate the collection by ensuring that the APY is not ridiculous + _validateCollection( + connectorBalance, + connectorBalance - lastBalance_, + timeSinceLastPoke + ); + } + + // 3. Level the assets to Fraction (connector) & 100-fraction (raw) + uint256 sum = _data.rawBalance + connectorBalance; + uint256 ideal = sum.mulTruncate(_data.fraction); + // If there is not enough mAsset in the connector, then deposit + if (ideal > connectorBalance) { + uint256 deposit = ideal - connectorBalance; + underlying.approve(address(connector_), deposit); + connector_.deposit(deposit); + } + // Else withdraw, if there is too much mAsset in the connector + else if (connectorBalance > ideal) { + // If fraction == 0, then withdraw everything + if (ideal == 0) { + connector_.withdrawAll(); + sum = IERC20(underlying).balanceOf(address(this)); + } else { + connector_.withdraw(connectorBalance - ideal); + } + } + // Else ideal == connectorBalance (e.g. 0), do nothing + require(connector_.checkBalance() >= ideal, "Enforce system invariant"); + + // 4i. Refresh exchange rate and emit event + lastBalance = ideal; + _refreshExchangeRate(sum, _data.totalCredits, false); + emit Poked(lastBalance_, ideal, connectorBalance - lastBalance_); + } else { + // 4ii. Refresh exchange rate and emit event + lastBalance = 0; + _refreshExchangeRate(_data.rawBalance, _data.totalCredits, false); + emit PokedRaw(); + } + } + + /** + * @dev Internal fn to refresh the exchange rate, based on the sum of collateral and the number of credits + * @param _realSum Sum of collateral held by the contract + * @param _totalCredits Total number of credits in the system + * @param _ignoreValidation This is for use in the emergency situation, and ignores a decreasing exchangeRate + */ + function _refreshExchangeRate( + uint256 _realSum, + uint256 _totalCredits, + bool _ignoreValidation + ) internal { + // Based on the current exchange rate, how much underlying is collateralised? + (uint256 totalCredited, ) = _creditsToUnderlying(_totalCredits); + + // Require the amount of capital held to be greater than the previously credited units + require(_ignoreValidation || _realSum >= totalCredited, "ExchangeRate must increase"); + // Work out the new exchange rate based on the current capital + uint256 newExchangeRate = _calcExchangeRate(_realSum, _totalCredits); + exchangeRate = newExchangeRate; + + emit ExchangeRateUpdated( + newExchangeRate, + _realSum > totalCredited ? _realSum - totalCredited : 0 + ); + } + + /** + * FORKED DIRECTLY FROM SAVINGSMANAGER.sol + * --------------------------------------- + * @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 _newBalance New balance of the underlying + * @param _interest Increase in total supply since last collection + * @param _timeSinceLastCollection Seconds since last collection + */ + function _validateCollection( + uint256 _newBalance, + uint256 _interest, + uint256 _timeSinceLastCollection + ) internal pure returns (uint256 extrapolatedAPY) { + // Protect against division by 0 + uint256 protectedTime = StableMath.max(1, _timeSinceLastCollection); + + uint256 oldSupply = _newBalance - _interest; + uint256 percentageIncrease = _interest.divPrecisely(oldSupply); + + uint256 yearsSinceLastCollection = protectedTime.divPrecisely(SECONDS_IN_YEAR); + + extrapolatedAPY = percentageIncrease.divPrecisely(yearsSinceLastCollection); + + if (protectedTime > 30 minutes) { + require(extrapolatedAPY < MAX_APY, "Interest protected from inflating past maxAPY"); + } else { + require(percentageIncrease < 1e15, "Interest protected from inflating past 10 Bps"); + } + } + + /*************************************** + VIEW - I + ****************************************/ + + struct CachedData { + // SLOAD from 'fraction' + uint256 fraction; + // ERC20 balance of underlying, held by this contract + // underlying.balanceOf(address(this)) + uint256 rawBalance; + // totalSupply() + uint256 totalCredits; + } + + /** + * @dev Retrieves generic data to avoid duplicate SLOADs + */ + function _cacheData() internal view returns (CachedData memory) { + uint256 balance = underlying.balanceOf(address(this)); + return CachedData(fraction, balance, totalSupply()); + } + + /** + * @dev Converts masset amount into credits based on exchange rate + * c = (masset / exchangeRate) + 1 + */ + function _underlyingToCredits(uint256 _underlying) + internal + view + returns (uint256 credits, uint256 exchangeRate_) + { + // e.g. (1e20 * 1e18) / 1e18 = 1e20 + // e.g. (1e20 * 1e18) / 14e17 = 7.1429e19 + // e.g. 1 * 1e18 / 1e17 + 1 = 11 => 11 * 1e17 / 1e18 = 1.1e18 / 1e18 = 1 + exchangeRate_ = exchangeRate; + credits = _underlying.divPrecisely(exchangeRate_) + 1; + } + + /** + * @dev Works out a new exchange rate, given an amount of collateral and total credits + * e = underlying / (credits-1) + */ + function _calcExchangeRate(uint256 _totalCollateral, uint256 _totalCredits) + internal + pure + returns (uint256 _exchangeRate) + { + _exchangeRate = _totalCollateral.divPrecisely(_totalCredits - 1); + } + + /** + * @dev Converts credit amount into masset based on exchange rate + * m = credits * exchangeRate + */ + function _creditsToUnderlying(uint256 _credits) + internal + view + returns (uint256 underlyingAmount, uint256 exchangeRate_) + { + // e.g. (1e20 * 1e18) / 1e18 = 1e20 + // e.g. (1e20 * 14e17) / 1e18 = 1.4e20 + exchangeRate_ = exchangeRate; + underlyingAmount = _credits.mulTruncate(exchangeRate_); + } +} diff --git a/contracts/legacy-upgraded/imusd-mainnet-22.sol b/contracts/legacy-upgraded/imusd-mainnet-22.sol new file mode 100644 index 00000000..daed924c --- /dev/null +++ b/contracts/legacy-upgraded/imusd-mainnet-22.sol @@ -0,0 +1,1859 @@ +pragma solidity 0.5.16; + +interface IUnwrapper { + // @dev Get bAssetOut status + function getIsBassetOut( + address _masset, + bool _inputIsCredit, + address _output + ) external view returns (bool isBassetOut); + + /// @dev Estimate output + function getUnwrapOutput( + bool _isBassetOut, + address _router, + address _input, + bool _inputIsCredit, + address _output, + uint256 _amount + ) external view returns (uint256 output); + + /// @dev Unwrap and send + function unwrapAndSend( + bool _isBassetOut, + address _router, + address _input, + address _output, + uint256 _amount, + uint256 _minAmountOut, + address _beneficiary + ) external returns (uint256 outputQuantity); +} + +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; +} + +interface ISavingsContractV1 { + function depositInterest(uint256 _amount) external; + + function depositSavings(uint256 _amount) external returns (uint256 creditsIssued); + + function redeem(uint256 _amount) external returns (uint256 massetReturned); + + function exchangeRate() external view returns (uint256); + + function creditBalances(address) external view returns (uint256); +} + +interface ISavingsContractV3 { + // 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 balance); // V2 + + function underlyingToCredits(uint256 _credits) external view returns (uint256 underlying); // V2 + + function creditsToUnderlying(uint256 _underlying) external view returns (uint256 credits); // V2 + + // -------------------------------------------- + + function redeemAndUnwrap( + uint256 _amount, + bool _isCreditAmt, + uint256 _minAmountOut, + address _output, + address _beneficiary, + address _router, + bool _isBassetOut + ) + external + returns ( + uint256 creditsBurned, + uint256 massetRedeemed, + uint256 outputQuantity + ); + + function depositSavings( + uint256 _underlying, + address _beneficiary, + address _referrer + ) external returns (uint256 creditsIssued); +} + +/* + * @dev Provides information about the current execution context, including the + * sender of the transaction and its data. While these are generally available + * via msg.sender and msg.data, they should not be accessed in such a direct + * manner, since when dealing with GSN meta-transactions the account sending and + * paying for execution may not be the actual sender (as far as an application + * is concerned). + * + * This contract is only required for intermediate, library-like contracts. + */ +contract Context { + // Empty internal constructor, to prevent people from mistakenly deploying + // an instance of this contract, which should be used via inheritance. + constructor() internal {} + + // solhint-disable-previous-line no-empty-blocks + + function _msgSender() internal view returns (address payable) { + return msg.sender; + } + + function _msgData() internal view returns (bytes memory) { + this; // silence state mutability warning without generating bytecode - see https://github.com/ethereum/solidity/issues/2691 + return msg.data; + } +} + +/** + * @dev Interface of the ERC20 standard as defined in the EIP. Does not include + * the optional functions; to access them see {ERC20Detailed}. + */ +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); +} + +/** + * @dev Wrappers over Solidity's arithmetic operations with added overflow + * checks. + * + * Arithmetic operations in Solidity wrap on overflow. This can easily result + * in bugs, because programmers usually assume that an overflow raises an + * error, which is the standard behavior in high level programming languages. + * `SafeMath` restores this intuition by reverting the transaction when an + * operation overflows. + * + * Using this library instead of the unchecked operations eliminates an entire + * class of bugs, so it's recommended to use it always. + */ +library SafeMath { + /** + * @dev Returns the addition of two unsigned integers, reverting on + * overflow. + * + * Counterpart to Solidity's `+` operator. + * + * Requirements: + * - Addition cannot overflow. + */ + function add(uint256 a, uint256 b) internal pure returns (uint256) { + uint256 c = a + b; + require(c >= a, "SafeMath: addition overflow"); + + return c; + } + + /** + * @dev Returns the subtraction of two unsigned integers, reverting on + * overflow (when the result is negative). + * + * Counterpart to Solidity's `-` operator. + * + * Requirements: + * - Subtraction cannot overflow. + */ + function sub(uint256 a, uint256 b) internal pure returns (uint256) { + return sub(a, b, "SafeMath: subtraction overflow"); + } + + /** + * @dev Returns the subtraction of two unsigned integers, reverting with custom message on + * overflow (when the result is negative). + * + * Counterpart to Solidity's `-` operator. + * + * Requirements: + * - Subtraction cannot overflow. + * + * _Available since v2.4.0._ + */ + function sub( + uint256 a, + uint256 b, + string memory errorMessage + ) internal pure returns (uint256) { + require(b <= a, errorMessage); + uint256 c = a - b; + + return c; + } + + /** + * @dev Returns the multiplication of two unsigned integers, reverting on + * overflow. + * + * Counterpart to Solidity's `*` operator. + * + * Requirements: + * - Multiplication cannot overflow. + */ + function mul(uint256 a, uint256 b) internal pure returns (uint256) { + // Gas optimization: this is cheaper than requiring 'a' not being zero, but the + // benefit is lost if 'b' is also tested. + // See: https://github.com/OpenZeppelin/openzeppelin-contracts/pull/522 + if (a == 0) { + return 0; + } + + uint256 c = a * b; + require(c / a == b, "SafeMath: multiplication overflow"); + + return c; + } + + /** + * @dev Returns the integer division of two unsigned integers. Reverts on + * division by zero. The result is rounded towards zero. + * + * Counterpart to Solidity's `/` operator. Note: this function uses a + * `revert` opcode (which leaves remaining gas untouched) while Solidity + * uses an invalid opcode to revert (consuming all remaining gas). + * + * Requirements: + * - The divisor cannot be zero. + */ + function div(uint256 a, uint256 b) internal pure returns (uint256) { + return div(a, b, "SafeMath: division by zero"); + } + + /** + * @dev Returns the integer division of two unsigned integers. Reverts with custom message on + * division by zero. The result is rounded towards zero. + * + * Counterpart to Solidity's `/` operator. Note: this function uses a + * `revert` opcode (which leaves remaining gas untouched) while Solidity + * uses an invalid opcode to revert (consuming all remaining gas). + * + * Requirements: + * - The divisor cannot be zero. + * + * _Available since v2.4.0._ + */ + function div( + uint256 a, + uint256 b, + string memory errorMessage + ) internal pure returns (uint256) { + // Solidity only automatically asserts when dividing by 0 + require(b > 0, errorMessage); + uint256 c = a / b; + // assert(a == b * c + a % b); // There is no case in which this doesn't hold + + return c; + } + + /** + * @dev Returns the remainder of dividing two unsigned integers. (unsigned integer modulo), + * Reverts when dividing by zero. + * + * Counterpart to Solidity's `%` operator. This function uses a `revert` + * opcode (which leaves remaining gas untouched) while Solidity uses an + * invalid opcode to revert (consuming all remaining gas). + * + * Requirements: + * - The divisor cannot be zero. + */ + function mod(uint256 a, uint256 b) internal pure returns (uint256) { + return mod(a, b, "SafeMath: modulo by zero"); + } + + /** + * @dev Returns the remainder of dividing two unsigned integers. (unsigned integer modulo), + * Reverts with custom message when dividing by zero. + * + * Counterpart to Solidity's `%` operator. This function uses a `revert` + * opcode (which leaves remaining gas untouched) while Solidity uses an + * invalid opcode to revert (consuming all remaining gas). + * + * Requirements: + * - The divisor cannot be zero. + * + * _Available since v2.4.0._ + */ + function mod( + uint256 a, + uint256 b, + string memory errorMessage + ) internal pure returns (uint256) { + require(b != 0, errorMessage); + return a % b; + } +} + +contract ERC20 is Context, IERC20 { + using SafeMath for uint256; + + mapping(address => uint256) private _balances; + + mapping(address => mapping(address => uint256)) private _allowances; + + uint256 private _totalSupply; + + /** + * @dev See {IERC20-totalSupply}. + */ + function totalSupply() public view returns (uint256) { + return _totalSupply; + } + + /** + * @dev See {IERC20-balanceOf}. + */ + function balanceOf(address account) public view returns (uint256) { + return _balances[account]; + } + + /** + * @dev See {IERC20-transfer}. + * + * Requirements: + * + * - `recipient` cannot be the zero address. + * - the caller must have a balance of at least `amount`. + */ + function transfer(address recipient, uint256 amount) public returns (bool) { + _transfer(_msgSender(), recipient, amount); + return true; + } + + /** + * @dev See {IERC20-allowance}. + */ + function allowance(address owner, address spender) public view returns (uint256) { + return _allowances[owner][spender]; + } + + /** + * @dev See {IERC20-approve}. + * + * Requirements: + * + * - `spender` cannot be the zero address. + */ + function approve(address spender, uint256 amount) public returns (bool) { + _approve(_msgSender(), spender, amount); + return true; + } + + /** + * @dev See {IERC20-transferFrom}. + * + * Emits an {Approval} event indicating the updated allowance. This is not + * required by the EIP. See the note at the beginning of {ERC20}; + * + * Requirements: + * - `sender` and `recipient` cannot be the zero address. + * - `sender` must have a balance of at least `amount`. + * - the caller must have allowance for `sender`'s tokens of at least + * `amount`. + */ + function transferFrom( + address sender, + address recipient, + uint256 amount + ) public returns (bool) { + _transfer(sender, recipient, amount); + _approve( + sender, + _msgSender(), + _allowances[sender][_msgSender()].sub( + amount, + "ERC20: transfer amount exceeds allowance" + ) + ); + return true; + } + + /** + * @dev Atomically increases the allowance granted to `spender` by the caller. + * + * This is an alternative to {approve} that can be used as a mitigation for + * problems described in {IERC20-approve}. + * + * Emits an {Approval} event indicating the updated allowance. + * + * Requirements: + * + * - `spender` cannot be the zero address. + */ + function increaseAllowance(address spender, uint256 addedValue) public returns (bool) { + _approve(_msgSender(), spender, _allowances[_msgSender()][spender].add(addedValue)); + return true; + } + + /** + * @dev Atomically decreases the allowance granted to `spender` by the caller. + * + * This is an alternative to {approve} that can be used as a mitigation for + * problems described in {IERC20-approve}. + * + * Emits an {Approval} event indicating the updated allowance. + * + * Requirements: + * + * - `spender` cannot be the zero address. + * - `spender` must have allowance for the caller of at least + * `subtractedValue`. + */ + function decreaseAllowance(address spender, uint256 subtractedValue) public returns (bool) { + _approve( + _msgSender(), + spender, + _allowances[_msgSender()][spender].sub( + subtractedValue, + "ERC20: decreased allowance below zero" + ) + ); + return true; + } + + /** + * @dev Moves tokens `amount` from `sender` to `recipient`. + * + * This is internal function is equivalent to {transfer}, and can be used to + * e.g. implement automatic token fees, slashing mechanisms, etc. + * + * Emits a {Transfer} event. + * + * Requirements: + * + * - `sender` cannot be the zero address. + * - `recipient` cannot be the zero address. + * - `sender` must have a balance of at least `amount`. + */ + function _transfer( + address sender, + address recipient, + uint256 amount + ) internal { + require(sender != address(0), "ERC20: transfer from the zero address"); + require(recipient != address(0), "ERC20: transfer to the zero address"); + + _balances[sender] = _balances[sender].sub(amount, "ERC20: transfer amount exceeds balance"); + _balances[recipient] = _balances[recipient].add(amount); + emit Transfer(sender, recipient, amount); + } + + /** @dev Creates `amount` tokens and assigns them to `account`, increasing + * the total supply. + * + * Emits a {Transfer} event with `from` set to the zero address. + * + * Requirements + * + * - `to` cannot be the zero address. + */ + function _mint(address account, uint256 amount) internal { + require(account != address(0), "ERC20: mint to the zero address"); + + _totalSupply = _totalSupply.add(amount); + _balances[account] = _balances[account].add(amount); + emit Transfer(address(0), account, amount); + } + + /** + * @dev Destroys `amount` tokens from `account`, reducing the + * total supply. + * + * Emits a {Transfer} event with `to` set to the zero address. + * + * Requirements + * + * - `account` cannot be the zero address. + * - `account` must have at least `amount` tokens. + */ + function _burn(address account, uint256 amount) internal { + require(account != address(0), "ERC20: burn from the zero address"); + + _balances[account] = _balances[account].sub(amount, "ERC20: burn amount exceeds balance"); + _totalSupply = _totalSupply.sub(amount); + emit Transfer(account, address(0), amount); + } + + /** + * @dev Sets `amount` as the allowance of `spender` over the `owner`s tokens. + * + * This is internal function is equivalent to `approve`, and can be used to + * e.g. set automatic allowances for certain subsystems, etc. + * + * Emits an {Approval} event. + * + * Requirements: + * + * - `owner` cannot be the zero address. + * - `spender` cannot be the zero address. + */ + function _approve( + address owner, + address spender, + uint256 amount + ) internal { + require(owner != address(0), "ERC20: approve from the zero address"); + require(spender != address(0), "ERC20: approve to the zero address"); + + _allowances[owner][spender] = amount; + emit Approval(owner, spender, amount); + } + + /** + * @dev Destroys `amount` tokens from `account`.`amount` is then deducted + * from the caller's allowance. + * + * See {_burn} and {_approve}. + */ + function _burnFrom(address account, uint256 amount) internal { + _burn(account, amount); + _approve( + account, + _msgSender(), + _allowances[account][_msgSender()].sub(amount, "ERC20: burn amount exceeds allowance") + ); + } +} + +contract InitializableERC20Detailed is IERC20 { + string private _name; + string private _symbol; + uint8 private _decimals; + + /** + * @dev Sets the values for `name`, `symbol`, and `decimals`. All three of + * these values are immutable: they can only be set once during + * construction. + * @notice To avoid variable shadowing appended `Arg` after arguments name. + */ + function _initialize( + string memory nameArg, + string memory symbolArg, + uint8 decimalsArg + ) internal { + _name = nameArg; + _symbol = symbolArg; + _decimals = decimalsArg; + } + + /** + * @dev Returns the name of the token. + */ + function name() public view returns (string memory) { + return _name; + } + + /** + * @dev Returns the symbol of the token, usually a shorter version of the + * name. + */ + function symbol() public view returns (string memory) { + return _symbol; + } + + /** + * @dev Returns the number of decimals used to get its user representation. + * For example, if `decimals` equals `2`, a balance of `505` tokens should + * be displayed to a user as `5,05` (`505 / 10 ** 2`). + * + * Tokens usually opt for a value of 18, imitating the relationship between + * Ether and Wei. + * + * NOTE: This information is only used for _display_ purposes: it in + * no way affects any of the arithmetic of the contract, including + * {IERC20-balanceOf} and {IERC20-transfer}. + */ + function decimals() public view returns (uint8) { + return _decimals; + } +} + +contract InitializableToken is ERC20, InitializableERC20Detailed { + /** + * @dev Initialization function for implementing contract + * @notice To avoid variable shadowing appended `Arg` after arguments name. + */ + function _initialize(string memory _nameArg, string memory _symbolArg) internal { + InitializableERC20Detailed._initialize(_nameArg, _symbolArg, 18); + } +} + +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; +} + +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; +} + +contract InitializableModule2 is ModuleKeys { + INexus public constant nexus = INexus(0xAFcE80b19A8cE13DEc0739a1aaB7A028d6845Eb3); + + /** + * @dev Modifier to allow function calls only from the Governor. + */ + modifier onlyGovernor() { + 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 Modifier to allow function calls only from the ProxyAdmin. + */ + modifier onlyProxyAdmin() { + require(msg.sender == _proxyAdmin(), "Only ProxyAdmin can execute"); + _; + } + + /** + * @dev Modifier to allow function calls only from the Manager. + */ + modifier onlyManager() { + require(msg.sender == _manager(), "Only manager 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 Staking Module address from the Nexus + * @return Address of the Staking Module contract + */ + function _staking() internal view returns (address) { + return nexus.getModule(KEY_STAKING); + } + + /** + * @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); + } + + /** + * @dev Return MetaToken Module address from the Nexus + * @return Address of the MetaToken Module contract + */ + function _metaToken() internal view returns (address) { + return nexus.getModule(KEY_META_TOKEN); + } + + /** + * @dev Return OracleHub Module address from the Nexus + * @return Address of the OracleHub Module contract + */ + function _oracleHub() internal view returns (address) { + return nexus.getModule(KEY_ORACLE_HUB); + } + + /** + * @dev Return Manager Module address from the Nexus + * @return Address of the Manager Module contract + */ + function _manager() internal view returns (address) { + return nexus.getModule(KEY_MANAGER); + } + + /** + * @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); + } +} + +interface IConnector { + /** + * @notice Deposits the mAsset into the connector + * @param _amount Units of mAsset to receive and deposit + */ + function deposit(uint256 _amount) external; + + /** + * @notice Withdraws a specific amount of mAsset from the connector + * @param _amount Units of mAsset to withdraw + */ + function withdraw(uint256 _amount) external; + + /** + * @notice Withdraws all mAsset from the connector + */ + function withdrawAll() external; + + /** + * @notice Returns the available balance in the connector. In connections + * where there is likely to be an initial dip in value due to conservative + * exchange rates (e.g. with Curves `get_virtual_price`), it should return + * max(deposited, balance) to avoid temporary negative yield. Any negative yield + * should be corrected during a withdrawal or over time. + * @return Balance of mAsset in the connector + */ + function checkBalance() external view returns (uint256); +} + +contract Initializable { + /** + * @dev Indicates that the contract has been initialized. + */ + bool private initialized; + + /** + * @dev Indicates that the contract is in the process of being initialized. + */ + bool private initializing; + + /** + * @dev Modifier to use in the initializer function of a contract. + */ + modifier initializer() { + require( + initializing || isConstructor() || !initialized, + "Contract instance has already been initialized" + ); + + bool isTopLevelCall = !initializing; + if (isTopLevelCall) { + initializing = true; + initialized = true; + } + + _; + + if (isTopLevelCall) { + initializing = false; + } + } + + /// @dev Returns true if and only if the function is running in the constructor + function isConstructor() private view returns (bool) { + // extcodesize checks the size of the code stored in an address, and + // address returns the current address. Since the code is still not + // deployed when running a constructor, any checks on its code size will + // yield zero, making it an effective way to detect if a contract is + // under construction or not. + address self = address(this); + uint256 cs; + assembly { + cs := extcodesize(self) + } + return cs == 0; + } + + // Reserved storage space to allow for layout changes in the future. + uint256[50] private ______gap; +} + +library StableMath { + using SafeMath for uint256; + + /** + * @dev Scaling unit for use in specific calculations, + * where 1 * 10**18, or 1e18 represents a unit '1' + */ + uint256 private constant FULL_SCALE = 1e18; + + /** + * @notice 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) + * @dev 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.mul(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 + uint256 z = x.mul(y); + // return 9e38 / 1e18 = 9e18 + return z.div(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.mul(y); + // e.g. 138145381104e17 + 9.99...e17 = 138145381113.99...e17 + uint256 ceil = scaled.add(FULL_SCALE.sub(1)); + // e.g. 13814538111.399...e18 / 1e18 = 13814538111 + return ceil.div(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 + uint256 z = x.mul(FULL_SCALE); + // e.g. 8e36 / 10e18 = 8e17 + return z.div(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 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.mul(ratio); + // 1e26 + 9.99e7 = 100..00.999e8 + uint256 ceil = scaled.add(RATIO_SCALE.sub(1)); + // return 100..00.999e8 / 1e8 = 1e18 + return ceil.div(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 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 + uint256 y = x.mul(RATIO_SCALE); + // return 1e22 / 1e12 = 1e10 + return y.div(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; + } +} + +/** + * @title SavingsContract + * @author Stability Labs Pty. Ltd. + * @notice Savings contract uses the ever increasing "exchangeRate" to increase + * the value of the Savers "credits" (ERC20) relative to the amount of additional + * underlying collateral that has been deposited into this contract ("interest") + * @dev VERSION: 2.1 + * DATE: 2021-11-25 + */ +contract SavingsContract_imusd_mainnet_22 is + ISavingsContractV1, + ISavingsContractV3, + Initializable, + InitializableToken, + InitializableModule2 +{ + using SafeMath for uint256; + using StableMath for uint256; + + // Core events for depositing and withdrawing + event ExchangeRateUpdated(uint256 newExchangeRate, uint256 interestCollected); + event SavingsDeposited(address indexed saver, uint256 savingsDeposited, uint256 creditsIssued); + event CreditsRedeemed( + address indexed redeemer, + uint256 creditsRedeemed, + uint256 savingsCredited + ); + + event AutomaticInterestCollectionSwitched(bool automationEnabled); + + // Connector poking + event PokerUpdated(address poker); + + event FractionUpdated(uint256 fraction); + event ConnectorUpdated(address connector); + event EmergencyUpdate(); + + event Poked(uint256 oldBalance, uint256 newBalance, uint256 interestDetected); + event PokedRaw(); + + // Tracking events + event Referral(address indexed referrer, address beneficiary, uint256 amount); + + // Rate between 'savings credits' and underlying + // e.g. 1 credit (1e17) mulTruncate(exchangeRate) = underlying, starts at 10:1 + // exchangeRate increases over time + uint256 private constant startingRate = 1e17; + uint256 public exchangeRate; + + // Underlying asset is underlying + IERC20 public constant underlying = IERC20(0xe2f2a5C287993345a840Db3B0845fbC70f5935a5); + bool private automateInterestCollection; + + // Yield + // Poker is responsible for depositing/withdrawing from connector + address public poker; + // Last time a poke was made + uint256 public lastPoke; + // Last known balance of the connector + uint256 public lastBalance; + // Fraction of capital assigned to the connector (100% = 1e18) + uint256 public fraction; + // Address of the current connector (all IConnectors are mStable validated) + IConnector public connector; + // How often do we allow pokes + uint256 private constant POKE_CADENCE = 4 hours; + // Max APY generated on the capital in the connector + uint256 private constant MAX_APY = 4e18; + uint256 private constant SECONDS_IN_YEAR = 365 days; + // Proxy contract for easy redemption + address public unwrapper; // TODO!! + + // Add these constants to bytecode at deploytime + function initialize( + address _poker, + string calldata _nameArg, + string calldata _symbolArg + ) external initializer { + InitializableToken._initialize(_nameArg, _symbolArg); + + require(_poker != address(0), "Invalid poker address"); + poker = _poker; + + fraction = 2e17; + automateInterestCollection = true; + exchangeRate = startingRate; + } + + /** @dev Only the savings managaer (pulled from Nexus) can execute this */ + modifier onlySavingsManager() { + require(msg.sender == _savingsManager(), "Only savings manager can execute"); + _; + } + + /*************************************** + VIEW - E + ****************************************/ + + /** + * @dev Returns the underlying balance of a given user + * @param _user Address of the user to check + * @return balance Units of underlying owned by the user + */ + function balanceOfUnderlying(address _user) external view returns (uint256 balance) { + (balance, ) = _creditsToUnderlying(balanceOf(_user)); + } + + /** + * @dev Converts a given underlying amount into credits + * @param _underlying Units of underlying + * @return credits Credit units (a.k.a imUSD) + */ + function underlyingToCredits(uint256 _underlying) external view returns (uint256 credits) { + (credits, ) = _underlyingToCredits(_underlying); + } + + /** + * @dev Converts a given credit amount into underlying + * @param _credits Units of credits + * @return amount Corresponding underlying amount + */ + function creditsToUnderlying(uint256 _credits) external view returns (uint256 amount) { + (amount, ) = _creditsToUnderlying(_credits); + } + + // Deprecated in favour of `balanceOf(address)` + // Maintained for backwards compatibility + // Returns the credit balance of a given user + function creditBalances(address _user) external view returns (uint256) { + return balanceOf(_user); + } + + /*************************************** + INTEREST + ****************************************/ + + /** + * @dev Deposit interest (add to savings) and update exchange rate of contract. + * Exchange rate is calculated as the ratio between new savings q and credits: + * exchange rate = savings / credits + * + * @param _amount Units of underlying to add to the savings vault + */ + function depositInterest(uint256 _amount) external onlySavingsManager { + require(_amount > 0, "Must deposit something"); + + // Transfer the interest from sender to here + require(underlying.transferFrom(msg.sender, address(this), _amount), "Must receive tokens"); + + // Calc new exchange rate, protect against initialisation case + uint256 totalCredits = totalSupply(); + if (totalCredits > 0) { + // new exchange rate is relationship between _totalCredits & totalSavings + // _totalCredits * exchangeRate = totalSavings + // exchangeRate = totalSavings/_totalCredits + (uint256 totalCollat, ) = _creditsToUnderlying(totalCredits); + uint256 newExchangeRate = _calcExchangeRate(totalCollat.add(_amount), totalCredits); + exchangeRate = newExchangeRate; + + emit ExchangeRateUpdated(newExchangeRate, _amount); + } + } + + /** @dev Enable or disable the automation of fee collection during deposit process */ + function automateInterestCollectionFlag(bool _enabled) external onlyGovernor { + automateInterestCollection = _enabled; + emit AutomaticInterestCollectionSwitched(_enabled); + } + + /*************************************** + DEPOSIT + ****************************************/ + + /** + * @dev During a migration period, allow savers to deposit underlying here before the interest has been redirected + * @param _underlying Units of underlying to deposit into savings vault + * @param _beneficiary Immediately transfer the imUSD token to this beneficiary address + * @return creditsIssued Units of credits (imUSD) issued + */ + function preDeposit(uint256 _underlying, address _beneficiary) + external + returns (uint256 creditsIssued) + { + require(exchangeRate == startingRate, "Can only use this method before streaming begins"); + return _deposit(_underlying, _beneficiary, false); + } + + /** + * @dev Deposit the senders savings to the vault, and credit them internally with "credits". + * Credit amount is calculated as a ratio of deposit amount and exchange rate: + * credits = underlying / exchangeRate + * We will first update the internal exchange rate by collecting any interest generated on the underlying. + * @param _underlying Units of underlying to deposit into savings vault + * @return creditsIssued Units of credits (imUSD) issued + */ + function depositSavings(uint256 _underlying) external returns (uint256 creditsIssued) { + return _deposit(_underlying, msg.sender, true); + } + + /** + * @dev Deposit the senders savings to the vault, and credit them internally with "credits". + * Credit amount is calculated as a ratio of deposit amount and exchange rate: + * credits = underlying / exchangeRate + * We will first update the internal exchange rate by collecting any interest generated on the underlying. + * @param _underlying Units of underlying to deposit into savings vault + * @param _beneficiary Immediately transfer the imUSD token to this beneficiary address + * @return creditsIssued Units of credits (imUSD) issued + */ + function depositSavings(uint256 _underlying, address _beneficiary) + external + returns (uint256 creditsIssued) + { + return _deposit(_underlying, _beneficiary, true); + } + + /** + * @dev Overloaded `depositSavings` method with an optional referrer address. + * @param _underlying Units of underlying to deposit into savings vault + * @param _beneficiary Immediately transfer the imUSD token to this beneficiary address + * @param _referrer Referrer address for this deposit + * @return creditsIssued Units of credits (imUSD) issued + */ + function depositSavings( + uint256 _underlying, + address _beneficiary, + address _referrer + ) external returns (uint256 creditsIssued) { + emit Referral(_referrer, _beneficiary, _underlying); + return _deposit(_underlying, _beneficiary, true); + } + + /** + * @dev Internally deposit the _underlying from the sender and credit the beneficiary with new imUSD + */ + function _deposit( + uint256 _underlying, + address _beneficiary, + bool _collectInterest + ) internal returns (uint256 creditsIssued) { + require(_underlying > 0, "Must deposit something"); + require(_beneficiary != address(0), "Invalid beneficiary address"); + + // Collect recent interest generated by basket and update exchange rate + IERC20 mAsset = underlying; + if (_collectInterest) { + ISavingsManager(_savingsManager()).collectAndDistributeInterest(address(mAsset)); + } + + // Transfer tokens from sender to here + require(mAsset.transferFrom(msg.sender, address(this), _underlying), "Must receive tokens"); + + // Calc how many credits they receive based on currentRatio + (creditsIssued, ) = _underlyingToCredits(_underlying); + + // add credits to ERC20 balances + _mint(_beneficiary, creditsIssued); + + emit SavingsDeposited(_beneficiary, _underlying, creditsIssued); + } + + /*************************************** + REDEEM + ****************************************/ + + // Deprecated in favour of redeemCredits + // Maintaining backwards compatibility, this fn minimics the old redeem fn, in which + // credits are redeemed but the interest from the underlying is not collected. + function redeem(uint256 _credits) external returns (uint256 massetReturned) { + require(_credits > 0, "Must withdraw something"); + + (, uint256 payout) = _redeem(_credits, true, true); + + // Collect recent interest generated by basket and update exchange rate + if (automateInterestCollection) { + ISavingsManager(_savingsManager()).collectAndDistributeInterest(address(underlying)); + } + + return payout; + } + + /** + * @dev Redeem specific number of the senders "credits" in exchange for underlying. + * Payout amount is calculated as a ratio of credits and exchange rate: + * payout = credits * exchangeRate + * @param _credits Amount of credits to redeem + * @return massetReturned Units of underlying mAsset paid out + */ + function redeemCredits(uint256 _credits) external returns (uint256 massetReturned) { + require(_credits > 0, "Must withdraw something"); + + // Collect recent interest generated by basket and update exchange rate + if (automateInterestCollection) { + ISavingsManager(_savingsManager()).collectAndDistributeInterest(address(underlying)); + } + + (, uint256 payout) = _redeem(_credits, true, true); + + return payout; + } + + /** + * @dev Redeem credits into a specific amount of underlying. + * Credits needed to burn is calculated using: + * credits = underlying / exchangeRate + * @param _underlying Amount of underlying to redeem + * @return creditsBurned Units of credits burned from sender + */ + function redeemUnderlying(uint256 _underlying) external returns (uint256 creditsBurned) { + require(_underlying > 0, "Must withdraw something"); + + // Collect recent interest generated by basket and update exchange rate + if (automateInterestCollection) { + ISavingsManager(_savingsManager()).collectAndDistributeInterest(address(underlying)); + } + + // Ensure that the payout was sufficient + (uint256 credits, uint256 massetReturned) = _redeem(_underlying, false, true); + require(massetReturned == _underlying, "Invalid output"); + + return credits; + } + + /** + * @notice Redeem credits into a specific amount of underlying, unwrap + * into a selected output asset, and send to a beneficiary + * Credits needed to burn is calculated using: + * credits = underlying / exchangeRate + * @param _amount Units to redeem (either underlying or credit amount). + * @param _isCreditAmt `true` if `amount` is in credits. eg imUSD. `false` if `amount` is in underlying. eg mUSD. + * @param _minAmountOut Minimum amount of `output` tokens to unwrap for. This is to the same decimal places as the `output` token. + * @param _output Asset to receive in exchange for the redeemed mAssets. This can be a bAsset or a fAsset. For example: + - bAssets (USDC, DAI, sUSD or USDT) or fAssets (GUSD, BUSD, alUSD, FEI or RAI) for mainnet imUSD Vault. + - bAssets (USDC, DAI or USDT) or fAsset FRAX for Polygon imUSD Vault. + - bAssets (WBTC, sBTC or renBTC) or fAssets (HBTC or TBTCV2) for mainnet imBTC Vault. + * @param _beneficiary Address to send `output` tokens to. + * @param _router mAsset address if the output is a bAsset. Feeder Pool address if the output is a fAsset. + * @param _isBassetOut `true` if `output` is a bAsset. `false` if `output` is a fAsset. + * @return creditsBurned Units of credits burned from sender. eg imUSD or imBTC. + * @return massetReturned Units of the underlying mAssets that were redeemed or swapped for the output tokens. eg mUSD or mBTC. + * @return outputQuantity Units of `output` tokens sent to the beneficiary. + */ + function redeemAndUnwrap( + uint256 _amount, + bool _isCreditAmt, + uint256 _minAmountOut, + address _output, + address _beneficiary, + address _router, + bool _isBassetOut + ) + external + returns ( + uint256 creditsBurned, + uint256 massetReturned, + uint256 outputQuantity + ) + { + require(_amount > 0, "Must withdraw something"); + require(_output != address(0), "Output address is zero"); + require(_beneficiary != address(0), "Beneficiary address is zero"); + require(_router != address(0), "Router address is zero"); + + // Collect recent interest generated by basket and update exchange rate + if (automateInterestCollection) { + ISavingsManager(_savingsManager()).collectAndDistributeInterest(address(underlying)); + } + + // Ensure that the payout was sufficient + (creditsBurned, massetReturned) = _redeem(_amount, _isCreditAmt, false); + require( + _isCreditAmt ? creditsBurned == _amount : massetReturned == _amount, + "Invalid output" + ); + + // Approve wrapper to spend contract's underlying; just for this tx + underlying.approve(unwrapper, massetReturned); + + // Unwrap the underlying into `output` and transfer to `beneficiary` + outputQuantity = IUnwrapper(unwrapper).unwrapAndSend( + _isBassetOut, + _router, + address(underlying), + _output, + massetReturned, + _minAmountOut, + _beneficiary + ); + } + + /** + * @dev Internally burn the credits and send the underlying to msg.sender + */ + function _redeem( + uint256 _amt, + bool _isCreditAmt, + bool _transferUnderlying + ) internal returns (uint256 creditsBurned, uint256 massetReturned) { + // Centralise credit <> underlying calcs and minimise SLOAD count + uint256 credits_; + uint256 underlying_; + uint256 exchangeRate_; + // If the input is a credit amt, then calculate underlying payout and cache the exchangeRate + if (_isCreditAmt) { + credits_ = _amt; + (underlying_, exchangeRate_) = _creditsToUnderlying(_amt); + } + // If the input is in underlying, then calculate credits needed to burn + else { + underlying_ = _amt; + (credits_, exchangeRate_) = _underlyingToCredits(_amt); + } + + // Burn required credits from the sender FIRST + _burn(msg.sender, credits_); + // Optionally, transfer tokens from here to sender + if (_transferUnderlying) { + require(underlying.transfer(msg.sender, underlying_), "Must send tokens"); + } + + // If this withdrawal pushes the portion of stored collateral in the `connector` over a certain + // threshold (fraction + 20%), then this should trigger a _poke on the connector. This is to avoid + // a situation in which there is a rush on withdrawals for some reason, causing the connector + // balance to go up and thus having too large an exposure. + CachedData memory cachedData = _cacheData(); + ConnectorStatus memory status = _getConnectorStatus(cachedData, exchangeRate_); + if (status.inConnector > status.limit) { + _poke(cachedData, false); + } + + emit CreditsRedeemed(msg.sender, credits_, underlying_); + + return (credits_, underlying_); + } + + struct ConnectorStatus { + // Limit is the max amount of units allowed in the connector + uint256 limit; + // Derived balance of the connector + uint256 inConnector; + } + + /** + * @dev Derives the units of collateral held in the connector + * @param _data Struct containing data on balances + * @param _exchangeRate Current system exchange rate + * @return status Contains max amount of assets allowed in connector + */ + function _getConnectorStatus(CachedData memory _data, uint256 _exchangeRate) + internal + pure + returns (ConnectorStatus memory) + { + // Total units of underlying collateralised + uint256 totalCollat = _data.totalCredits.mulTruncate(_exchangeRate); + // Max amount of underlying that can be held in the connector + uint256 limit = totalCollat.mulTruncate(_data.fraction.add(2e17)); + // Derives amount of underlying present in the connector + uint256 inConnector = _data.rawBalance >= totalCollat + ? 0 + : totalCollat.sub(_data.rawBalance); + + return ConnectorStatus(limit, inConnector); + } + + /*************************************** + YIELD - E + ****************************************/ + + /** @dev Modifier allowing only the designated poker to execute the fn */ + modifier onlyPoker() { + require(msg.sender == poker, "Only poker can execute"); + _; + } + + /** + * @dev External poke function allows for the redistribution of collateral between here and the + * current connector, setting the ratio back to the defined optimal. + */ + function poke() external onlyPoker { + CachedData memory cachedData = _cacheData(); + _poke(cachedData, false); + } + + /** + * @dev Governance action to set the address of a new poker + * @param _newPoker Address of the new poker + */ + function setPoker(address _newPoker) external onlyGovernor { + require(_newPoker != address(0) && _newPoker != poker, "Invalid poker"); + + poker = _newPoker; + + emit PokerUpdated(_newPoker); + } + + /** + * @dev Governance action to set the percentage of assets that should be held + * in the connector. + * @param _fraction Percentage of assets that should be held there (where 20% == 2e17) + */ + function setFraction(uint256 _fraction) external onlyGovernor { + require(_fraction <= 5e17, "Fraction must be <= 50%"); + + fraction = _fraction; + + CachedData memory cachedData = _cacheData(); + _poke(cachedData, true); + + emit FractionUpdated(_fraction); + } + + /** + * @dev Governance action to set the address of a new connector, and move funds (if any) across. + * @param _newConnector Address of the new connector + */ + function setConnector(address _newConnector) external onlyGovernor { + // Withdraw all from previous by setting target = 0 + CachedData memory cachedData = _cacheData(); + cachedData.fraction = 0; + _poke(cachedData, true); + + // Set new connector + CachedData memory cachedDataNew = _cacheData(); + connector = IConnector(_newConnector); + _poke(cachedDataNew, true); + + emit ConnectorUpdated(_newConnector); + } + + /** + * @dev Governance action to perform an emergency withdraw of the assets in the connector, + * should it be the case that some or all of the liquidity is trapped in. This causes the total + * collateral in the system to go down, causing a hard refresh. + */ + function emergencyWithdraw(uint256 _withdrawAmount) external onlyGovernor { + // withdraw _withdrawAmount from connection + connector.withdraw(_withdrawAmount); + + // reset the connector + connector = IConnector(address(0)); + emit ConnectorUpdated(address(0)); + + // set fraction to 0 + fraction = 0; + emit FractionUpdated(0); + + // check total collateralisation of credits + CachedData memory data = _cacheData(); + // use rawBalance as the remaining liquidity in the connector is now written off + _refreshExchangeRate(data.rawBalance, data.totalCredits, true); + + emit EmergencyUpdate(); + } + + /*************************************** + YIELD - I + ****************************************/ + + /** @dev Internal poke function to keep the balance between connector and raw balance healthy */ + function _poke(CachedData memory _data, bool _ignoreCadence) internal { + require(_data.totalCredits > 0, "Must have something to poke"); + + // 1. Verify that poke cadence is valid, unless this is a manual action by governance + uint256 currentTime = uint256(now); + uint256 timeSinceLastPoke = currentTime.sub(lastPoke); + require(_ignoreCadence || timeSinceLastPoke > POKE_CADENCE, "Not enough time elapsed"); + lastPoke = currentTime; + + // If there is a connector, check the balance and settle to the specified fraction % + IConnector connector_ = connector; + if (address(connector_) != address(0)) { + // 2. Check and verify new connector balance + uint256 lastBalance_ = lastBalance; + uint256 connectorBalance = connector_.checkBalance(); + // Always expect the collateral in the connector to increase in value + require(connectorBalance >= lastBalance_, "Invalid yield"); + if (connectorBalance > 0) { + // Validate the collection by ensuring that the APY is not ridiculous + _validateCollection( + connectorBalance, + connectorBalance.sub(lastBalance_), + timeSinceLastPoke + ); + } + + // 3. Level the assets to Fraction (connector) & 100-fraction (raw) + uint256 sum = _data.rawBalance.add(connectorBalance); + uint256 ideal = sum.mulTruncate(_data.fraction); + // If there is not enough mAsset in the connector, then deposit + if (ideal > connectorBalance) { + uint256 deposit = ideal.sub(connectorBalance); + underlying.approve(address(connector_), deposit); + connector_.deposit(deposit); + } + // Else withdraw, if there is too much mAsset in the connector + else if (connectorBalance > ideal) { + // If fraction == 0, then withdraw everything + if (ideal == 0) { + connector_.withdrawAll(); + sum = IERC20(underlying).balanceOf(address(this)); + } else { + connector_.withdraw(connectorBalance.sub(ideal)); + } + } + // Else ideal == connectorBalance (e.g. 0), do nothing + require(connector_.checkBalance() >= ideal, "Enforce system invariant"); + + // 4i. Refresh exchange rate and emit event + lastBalance = ideal; + _refreshExchangeRate(sum, _data.totalCredits, false); + emit Poked(lastBalance_, ideal, connectorBalance.sub(lastBalance_)); + } else { + // 4ii. Refresh exchange rate and emit event + lastBalance = 0; + _refreshExchangeRate(_data.rawBalance, _data.totalCredits, false); + emit PokedRaw(); + } + } + + /** + * @dev Internal fn to refresh the exchange rate, based on the sum of collateral and the number of credits + * @param _realSum Sum of collateral held by the contract + * @param _totalCredits Total number of credits in the system + * @param _ignoreValidation This is for use in the emergency situation, and ignores a decreasing exchangeRate + */ + function _refreshExchangeRate( + uint256 _realSum, + uint256 _totalCredits, + bool _ignoreValidation + ) internal { + // Based on the current exchange rate, how much underlying is collateralised? + (uint256 totalCredited, ) = _creditsToUnderlying(_totalCredits); + + // Require the amount of capital held to be greater than the previously credited units + require(_ignoreValidation || _realSum >= totalCredited, "ExchangeRate must increase"); + // Work out the new exchange rate based on the current capital + uint256 newExchangeRate = _calcExchangeRate(_realSum, _totalCredits); + exchangeRate = newExchangeRate; + + emit ExchangeRateUpdated( + newExchangeRate, + _realSum > totalCredited ? _realSum.sub(totalCredited) : 0 + ); + } + + /** + * FORKED DIRECTLY FROM SAVINGSMANAGER.sol + * --------------------------------------- + * @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 _newBalance New balance of the underlying + * @param _interest Increase in total supply since last collection + * @param _timeSinceLastCollection Seconds since last collection + */ + function _validateCollection( + uint256 _newBalance, + uint256 _interest, + uint256 _timeSinceLastCollection + ) internal pure returns (uint256 extrapolatedAPY) { + // Protect against division by 0 + uint256 protectedTime = StableMath.max(1, _timeSinceLastCollection); + + uint256 oldSupply = _newBalance.sub(_interest); + uint256 percentageIncrease = _interest.divPrecisely(oldSupply); + + uint256 yearsSinceLastCollection = protectedTime.divPrecisely(SECONDS_IN_YEAR); + + extrapolatedAPY = percentageIncrease.divPrecisely(yearsSinceLastCollection); + + if (protectedTime > 30 minutes) { + require(extrapolatedAPY < MAX_APY, "Interest protected from inflating past maxAPY"); + } else { + require(percentageIncrease < 1e15, "Interest protected from inflating past 10 Bps"); + } + } + + /*************************************** + VIEW - I + ****************************************/ + + struct CachedData { + // SLOAD from 'fraction' + uint256 fraction; + // ERC20 balance of underlying, held by this contract + // underlying.balanceOf(address(this)) + uint256 rawBalance; + // totalSupply() + uint256 totalCredits; + } + + /** + * @dev Retrieves generic data to avoid duplicate SLOADs + */ + function _cacheData() internal view returns (CachedData memory) { + uint256 balance = underlying.balanceOf(address(this)); + return CachedData(fraction, balance, totalSupply()); + } + + /** + * @dev Converts masset amount into credits based on exchange rate + * c = (masset / exchangeRate) + 1 + */ + function _underlyingToCredits(uint256 _underlying) + internal + view + returns (uint256 credits, uint256 exchangeRate_) + { + // e.g. (1e20 * 1e18) / 1e18 = 1e20 + // e.g. (1e20 * 1e18) / 14e17 = 7.1429e19 + // e.g. 1 * 1e18 / 1e17 + 1 = 11 => 11 * 1e17 / 1e18 = 1.1e18 / 1e18 = 1 + exchangeRate_ = exchangeRate; + credits = _underlying.divPrecisely(exchangeRate_).add(1); + } + + /** + * @dev Works out a new exchange rate, given an amount of collateral and total credits + * e = underlying / (credits-1) + */ + function _calcExchangeRate(uint256 _totalCollateral, uint256 _totalCredits) + internal + pure + returns (uint256 _exchangeRate) + { + _exchangeRate = _totalCollateral.divPrecisely(_totalCredits.sub(1)); + } + + /** + * @dev Converts credit amount into masset based on exchange rate + * m = credits * exchangeRate + */ + function _creditsToUnderlying(uint256 _credits) + internal + view + returns (uint256 underlyingAmount, uint256 exchangeRate_) + { + // e.g. (1e20 * 1e18) / 1e18 = 1e20 + // e.g. (1e20 * 14e17) / 1e18 = 1.4e20 + exchangeRate_ = exchangeRate; + underlyingAmount = _credits.mulTruncate(exchangeRate_); + } +} diff --git a/contracts/legacy-upgraded/imusd-polygon-22.sol b/contracts/legacy-upgraded/imusd-polygon-22.sol new file mode 100644 index 00000000..0bbfc23b --- /dev/null +++ b/contracts/legacy-upgraded/imusd-polygon-22.sol @@ -0,0 +1,1699 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later +pragma solidity 0.8.2; + +interface IUnwrapper { + // @dev Get bAssetOut status + function getIsBassetOut( + address _masset, + bool _inputIsCredit, + address _output + ) external view returns (bool isBassetOut); + + /// @dev Estimate output + function getUnwrapOutput( + bool _isBassetOut, + address _router, + address _input, + bool _inputIsCredit, + address _output, + uint256 _amount + ) external view returns (uint256 output); + + /// @dev Unwrap and send + function unwrapAndSend( + bool _isBassetOut, + address _router, + address _input, + address _output, + uint256 _amount, + uint256 _minAmountOut, + address _beneficiary + ) external returns (uint256 outputQuantity); +} + +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); +} + +interface ISavingsContractV3 { + // 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 balance); // V2 + + function underlyingToCredits(uint256 _credits) external view returns (uint256 underlying); // V2 + + function creditsToUnderlying(uint256 _underlying) external view returns (uint256 credits); // V2 + + // -------------------------------------------- + + function redeemAndUnwrap( + uint256 _amount, + bool _isCreditAmt, + uint256 _minAmountOut, + address _output, + address _beneficiary, + address _router, + bool _isBassetOut + ) + external + returns ( + uint256 creditsBurned, + uint256 massetRedeemed, + uint256 outputQuantity + ); + + function depositSavings( + uint256 _underlying, + address _beneficiary, + address _referrer + ) external returns (uint256 creditsIssued); +} + +/* + * @dev Provides information about the current execution context, including the + * sender of the transaction and its data. While these are generally available + * via msg.sender and msg.data, they should not be accessed in such a direct + * manner, since when dealing with GSN meta-transactions the account sending and + * paying for execution may not be the actual sender (as far as an application + * is concerned). + * + * This contract is only required for intermediate, library-like contracts. + */ +abstract contract Context { + // Empty internal constructor, to prevent people from mistakenly deploying + // an instance of this contract, which should be used via inheritance. + // constructor () internal { } + // solhint-disable-previous-line no-empty-blocks + + function _msgSender() internal view returns (address payable) { + return payable(msg.sender); + } + + function _msgData() internal view returns (bytes memory) { + this; // silence state mutability warning without generating bytecode - see https://github.com/ethereum/solidity/issues/2691 + return msg.data; + } +} + +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); +} + +contract ERC205 is Context, IERC20 { + mapping(address => uint256) private _balances; + + mapping(address => mapping(address => uint256)) private _allowances; + + uint256 private _totalSupply; + + /** + * @dev See {IERC20-totalSupply}. + */ + function totalSupply() public view virtual override returns (uint256) { + return _totalSupply; + } + + /** + * @dev See {IERC20-balanceOf}. + */ + function balanceOf(address account) public view virtual override returns (uint256) { + return _balances[account]; + } + + /** + * @dev See {IERC20-transfer}. + * + * Requirements: + * + * - `recipient` cannot be the zero address. + * - the caller must have a balance of at least `amount`. + */ + function transfer(address recipient, uint256 amount) public virtual override returns (bool) { + _transfer(_msgSender(), recipient, amount); + return true; + } + + /** + * @dev See {IERC20-allowance}. + */ + function allowance(address owner, address spender) + public + view + virtual + override + returns (uint256) + { + return _allowances[owner][spender]; + } + + /** + * @dev See {IERC20-approve}. + * + * Requirements: + * + * - `spender` cannot be the zero address. + */ + function approve(address spender, uint256 amount) public virtual override returns (bool) { + _approve(_msgSender(), spender, amount); + return true; + } + + /** + * @dev See {IERC20-transferFrom}. + * + * Emits an {Approval} event indicating the updated allowance. This is not + * required by the EIP. See the note at the beginning of {ERC20}. + * + * Requirements: + * + * - `sender` and `recipient` cannot be the zero address. + * - `sender` must have a balance of at least `amount`. + * - the caller must have allowance for ``sender``'s tokens of at least + * `amount`. + */ + function transferFrom( + address sender, + address recipient, + uint256 amount + ) public virtual override returns (bool) { + _transfer(sender, recipient, amount); + + uint256 currentAllowance = _allowances[sender][_msgSender()]; + require(currentAllowance >= amount, "ERC20: transfer amount exceeds allowance"); + _approve(sender, _msgSender(), currentAllowance - amount); + + return true; + } + + /** + * @dev Atomically increases the allowance granted to `spender` by the caller. + * + * This is an alternative to {approve} that can be used as a mitigation for + * problems described in {IERC20-approve}. + * + * Emits an {Approval} event indicating the updated allowance. + * + * Requirements: + * + * - `spender` cannot be the zero address. + */ + function increaseAllowance(address spender, uint256 addedValue) public virtual returns (bool) { + _approve(_msgSender(), spender, _allowances[_msgSender()][spender] + addedValue); + return true; + } + + /** + * @dev Atomically decreases the allowance granted to `spender` by the caller. + * + * This is an alternative to {approve} that can be used as a mitigation for + * problems described in {IERC20-approve}. + * + * Emits an {Approval} event indicating the updated allowance. + * + * Requirements: + * + * - `spender` cannot be the zero address. + * - `spender` must have allowance for the caller of at least + * `subtractedValue`. + */ + function decreaseAllowance(address spender, uint256 subtractedValue) + public + virtual + returns (bool) + { + uint256 currentAllowance = _allowances[_msgSender()][spender]; + require(currentAllowance >= subtractedValue, "ERC20: decreased allowance below zero"); + _approve(_msgSender(), spender, currentAllowance - subtractedValue); + + return true; + } + + /** + * @dev Moves tokens `amount` from `sender` to `recipient`. + * + * This is internal function is equivalent to {transfer}, and can be used to + * e.g. implement automatic token fees, slashing mechanisms, etc. + * + * Emits a {Transfer} event. + * + * Requirements: + * + * - `sender` cannot be the zero address. + * - `recipient` cannot be the zero address. + * - `sender` must have a balance of at least `amount`. + */ + function _transfer( + address sender, + address recipient, + uint256 amount + ) internal virtual { + require(sender != address(0), "ERC20: transfer from the zero address"); + require(recipient != address(0), "ERC20: transfer to the zero address"); + + uint256 senderBalance = _balances[sender]; + require(senderBalance >= amount, "ERC20: transfer amount exceeds balance"); + _balances[sender] = senderBalance - amount; + _balances[recipient] += amount; + + emit Transfer(sender, recipient, amount); + } + + /** @dev Creates `amount` tokens and assigns them to `account`, increasing + * the total supply. + * + * Emits a {Transfer} event with `from` set to the zero address. + * + * Requirements: + * + * - `to` cannot be the zero address. + */ + function _mint(address account, uint256 amount) internal virtual { + require(account != address(0), "ERC20: mint to the zero address"); + + _totalSupply += amount; + _balances[account] += amount; + emit Transfer(address(0), account, amount); + } + + /** + * @dev Destroys `amount` tokens from `account`, reducing the + * total supply. + * + * Emits a {Transfer} event with `to` set to the zero address. + * + * Requirements: + * + * - `account` cannot be the zero address. + * - `account` must have at least `amount` tokens. + */ + function _burn(address account, uint256 amount) internal virtual { + require(account != address(0), "ERC20: burn from the zero address"); + + uint256 accountBalance = _balances[account]; + require(accountBalance >= amount, "ERC20: burn amount exceeds balance"); + _balances[account] = accountBalance - amount; + _totalSupply -= amount; + + emit Transfer(account, address(0), amount); + } + + /** + * @dev Sets `amount` as the allowance of `spender` over the `owner` s tokens. + * + * This internal function is equivalent to `approve`, and can be used to + * e.g. set automatic allowances for certain subsystems, etc. + * + * Emits an {Approval} event. + * + * Requirements: + * + * - `owner` cannot be the zero address. + * - `spender` cannot be the zero address. + */ + function _approve( + address owner, + address spender, + uint256 amount + ) internal virtual { + require(owner != address(0), "ERC20: approve from the zero address"); + require(spender != address(0), "ERC20: approve to the zero address"); + + _allowances[owner][spender] = amount; + emit Approval(owner, spender, amount); + } +} + +abstract contract InitializableERC20Detailed is IERC20 { + string private _name; + string private _symbol; + uint8 private _decimals; + + /** + * @dev Sets the values for `name`, `symbol`, and `decimals`. All three of + * these values are immutable: they can only be set once during + * construction. + * @notice To avoid variable shadowing appended `Arg` after arguments name. + */ + function _initialize( + string memory nameArg, + string memory symbolArg, + uint8 decimalsArg + ) internal { + _name = nameArg; + _symbol = symbolArg; + _decimals = decimalsArg; + } + + /** + * @dev Returns the name of the token. + */ + function name() public view returns (string memory) { + return _name; + } + + /** + * @dev Returns the symbol of the token, usually a shorter version of the + * name. + */ + function symbol() public view returns (string memory) { + return _symbol; + } + + /** + * @dev Returns the number of decimals used to get its user representation. + * For example, if `decimals` equals `2`, a balance of `505` tokens should + * be displayed to a user as `5,05` (`505 / 10 ** 2`). + * + * Tokens usually opt for a value of 18, imitating the relationship between + * Ether and Wei. + * + * NOTE: This information is only used for _display_ purposes: it in + * no way affects any of the arithmetic of the contract, including + * {IERC20-balanceOf} and {IERC20-transfer}. + */ + function decimals() public view returns (uint8) { + return _decimals; + } +} + +abstract contract InitializableToken is ERC205, InitializableERC20Detailed { + /** + * @dev Initialization function for implementing contract + * @notice To avoid variable shadowing appended `Arg` after arguments name. + */ + function _initialize(string memory _nameArg, string memory _symbolArg) internal { + InitializableERC20Detailed._initialize(_nameArg, _symbolArg, 18); + } +} + +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 Recollateraliser Module address from the Nexus + * @return Address of the Recollateraliser Module contract (Phase 2) + */ + 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); + } +} + +interface IConnector { + /** + * @notice Deposits the mAsset into the connector + * @param _amount Units of mAsset to receive and deposit + */ + function deposit(uint256 _amount) external; + + /** + * @notice Withdraws a specific amount of mAsset from the connector + * @param _amount Units of mAsset to withdraw + */ + function withdraw(uint256 _amount) external; + + /** + * @notice Withdraws all mAsset from the connector + */ + function withdrawAll() external; + + /** + * @notice Returns the available balance in the connector. In connections + * where there is likely to be an initial dip in value due to conservative + * exchange rates (e.g. with Curves `get_virtual_price`), it should return + * max(deposited, balance) to avoid temporary negative yield. Any negative yield + * should be corrected during a withdrawal or over time. + * @return Balance of mAsset in the connector + */ + function checkBalance() external view returns (uint256); +} + +contract Initializable { + /** + * @dev Indicates that the contract has been initialized. + */ + bool private initialized; + + /** + * @dev Indicates that the contract is in the process of being initialized. + */ + bool private initializing; + + /** + * @dev Modifier to use in the initializer function of a contract. + */ + modifier initializer() { + require( + initializing || isConstructor() || !initialized, + "Contract instance has already been initialized" + ); + + bool isTopLevelCall = !initializing; + if (isTopLevelCall) { + initializing = true; + initialized = true; + } + + _; + + if (isTopLevelCall) { + initializing = false; + } + } + + /// @dev Returns true if and only if the function is running in the constructor + function isConstructor() private view returns (bool) { + // extcodesize checks the size of the code stored in an address, and + // address returns the current address. Since the code is still not + // deployed when running a constructor, any checks on its code size will + // yield zero, making it an effective way to detect if a contract is + // under construction or not. + address self = address(this); + uint256 cs; + assembly { + cs := extcodesize(self) + } + return cs == 0; + } + + // Reserved storage space to allow for layout changes in the future. + uint256[50] private ______gap; +} + +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 9e38 / 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 SavingsContract + * @author mStable + * @notice Savings contract uses the ever increasing "exchangeRate" to increase + * the value of the Savers "credits" (ERC20) relative to the amount of additional + * underlying collateral that has been deposited into this contract ("interest") + * @dev VERSION: 2.1 + * DATE: 2021-11-25 + */ +contract SavingsContract_imusd_polygon_22 is + ISavingsContractV3, + Initializable, + InitializableToken, + ImmutableModule +{ + using StableMath for uint256; + + // Core events for depositing and withdrawing + event ExchangeRateUpdated(uint256 newExchangeRate, uint256 interestCollected); + event SavingsDeposited(address indexed saver, uint256 savingsDeposited, uint256 creditsIssued); + event CreditsRedeemed( + address indexed redeemer, + uint256 creditsRedeemed, + uint256 savingsCredited + ); + + event AutomaticInterestCollectionSwitched(bool automationEnabled); + + // Connector poking + event PokerUpdated(address poker); + + event FractionUpdated(uint256 fraction); + event ConnectorUpdated(address connector); + event EmergencyUpdate(); + + event Poked(uint256 oldBalance, uint256 newBalance, uint256 interestDetected); + event PokedRaw(); + + // Tracking events + event Referral(address indexed referrer, address beneficiary, uint256 amount); + + // Rate between 'savings credits' and underlying + // e.g. 1 credit (1e17) mulTruncate(exchangeRate) = underlying, starts at 10:1 + // exchangeRate increases over time + uint256 private constant startingRate = 1e17; + uint256 public override exchangeRate; + + // Underlying asset is underlying + IERC20 public immutable underlying; + bool private automateInterestCollection; + + // Yield + // Poker is responsible for depositing/withdrawing from connector + address public poker; + // Last time a poke was made + uint256 public lastPoke; + // Last known balance of the connector + uint256 public lastBalance; + // Fraction of capital assigned to the connector (100% = 1e18) + uint256 public fraction; + // Address of the current connector (all IConnectors are mStable validated) + IConnector public connector; + // How often do we allow pokes + uint256 private constant POKE_CADENCE = 4 hours; + // Max APY generated on the capital in the connector + uint256 private constant MAX_APY = 4e18; + uint256 private constant SECONDS_IN_YEAR = 365 days; + // Proxy contract for easy redemption + address public immutable unwrapper; + + constructor( + address _nexus, + address _underlying, + address _unwrapper + ) ImmutableModule(_nexus) { + require(_underlying != address(0), "mAsset address is zero"); + require(_unwrapper != address(0), "Unwrapper address is zero"); + underlying = IERC20(_underlying); + unwrapper = _unwrapper; + } + + // Add these constants to bytecode at deploytime + function initialize( + address _poker, + string calldata _nameArg, + string calldata _symbolArg + ) external initializer { + InitializableToken._initialize(_nameArg, _symbolArg); + + require(_poker != address(0), "Invalid poker address"); + poker = _poker; + + fraction = 2e17; + automateInterestCollection = true; + exchangeRate = startingRate; + } + + /** @dev Only the savings managaer (pulled from Nexus) can execute this */ + modifier onlySavingsManager() { + require(msg.sender == _savingsManager(), "Only savings manager can execute"); + _; + } + + /*************************************** + VIEW - E + ****************************************/ + + /** + * @dev Returns the underlying balance of a given user + * @param _user Address of the user to check + * @return balance Units of underlying owned by the user + */ + function balanceOfUnderlying(address _user) external view override returns (uint256 balance) { + (balance, ) = _creditsToUnderlying(balanceOf(_user)); + } + + /** + * @dev Converts a given underlying amount into credits + * @param _underlying Units of underlying + * @return credits Credit units (a.k.a imUSD) + */ + function underlyingToCredits(uint256 _underlying) + external + view + override + returns (uint256 credits) + { + (credits, ) = _underlyingToCredits(_underlying); + } + + /** + * @dev Converts a given credit amount into underlying + * @param _credits Units of credits + * @return amount Corresponding underlying amount + */ + function creditsToUnderlying(uint256 _credits) external view override returns (uint256 amount) { + (amount, ) = _creditsToUnderlying(_credits); + } + + // Deprecated in favour of `balanceOf(address)` + // Maintained for backwards compatibility + // Returns the credit balance of a given user + function creditBalances(address _user) external view override returns (uint256) { + return balanceOf(_user); + } + + /*************************************** + INTEREST + ****************************************/ + + /** + * @dev Deposit interest (add to savings) and update exchange rate of contract. + * Exchange rate is calculated as the ratio between new savings q and credits: + * exchange rate = savings / credits + * + * @param _amount Units of underlying to add to the savings vault + */ + function depositInterest(uint256 _amount) external override onlySavingsManager { + require(_amount > 0, "Must deposit something"); + + // Transfer the interest from sender to here + require(underlying.transferFrom(msg.sender, address(this), _amount), "Must receive tokens"); + + // Calc new exchange rate, protect against initialisation case + uint256 totalCredits = totalSupply(); + if (totalCredits > 0) { + // new exchange rate is relationship between _totalCredits & totalSavings + // _totalCredits * exchangeRate = totalSavings + // exchangeRate = totalSavings/_totalCredits + (uint256 totalCollat, ) = _creditsToUnderlying(totalCredits); + uint256 newExchangeRate = _calcExchangeRate(totalCollat + _amount, totalCredits); + exchangeRate = newExchangeRate; + + emit ExchangeRateUpdated(newExchangeRate, _amount); + } + } + + /** @dev Enable or disable the automation of fee collection during deposit process */ + function automateInterestCollectionFlag(bool _enabled) external onlyGovernor { + automateInterestCollection = _enabled; + emit AutomaticInterestCollectionSwitched(_enabled); + } + + /*************************************** + DEPOSIT + ****************************************/ + + /** + * @dev During a migration period, allow savers to deposit underlying here before the interest has been redirected + * @param _underlying Units of underlying to deposit into savings vault + * @param _beneficiary Immediately transfer the imUSD token to this beneficiary address + * @return creditsIssued Units of credits (imUSD) issued + */ + function preDeposit(uint256 _underlying, address _beneficiary) + external + returns (uint256 creditsIssued) + { + require(exchangeRate == startingRate, "Can only use this method before streaming begins"); + return _deposit(_underlying, _beneficiary, false); + } + + /** + * @dev Deposit the senders savings to the vault, and credit them internally with "credits". + * Credit amount is calculated as a ratio of deposit amount and exchange rate: + * credits = underlying / exchangeRate + * We will first update the internal exchange rate by collecting any interest generated on the underlying. + * @param _underlying Units of underlying to deposit into savings vault + * @return creditsIssued Units of credits (imUSD) issued + */ + function depositSavings(uint256 _underlying) external override returns (uint256 creditsIssued) { + return _deposit(_underlying, msg.sender, true); + } + + /** + * @dev Deposit the senders savings to the vault, and credit them internally with "credits". + * Credit amount is calculated as a ratio of deposit amount and exchange rate: + * credits = underlying / exchangeRate + * We will first update the internal exchange rate by collecting any interest generated on the underlying. + * @param _underlying Units of underlying to deposit into savings vault + * @param _beneficiary Immediately transfer the imUSD token to this beneficiary address + * @return creditsIssued Units of credits (imUSD) issued + */ + function depositSavings(uint256 _underlying, address _beneficiary) + external + override + returns (uint256 creditsIssued) + { + return _deposit(_underlying, _beneficiary, true); + } + + /** + * @dev Overloaded `depositSavings` method with an optional referrer address. + * @param _underlying Units of underlying to deposit into savings vault + * @param _beneficiary Immediately transfer the imUSD token to this beneficiary address + * @param _referrer Referrer address for this deposit + * @return creditsIssued Units of credits (imUSD) issued + */ + function depositSavings( + uint256 _underlying, + address _beneficiary, + address _referrer + ) external override returns (uint256 creditsIssued) { + emit Referral(_referrer, _beneficiary, _underlying); + return _deposit(_underlying, _beneficiary, true); + } + + /** + * @dev Internally deposit the _underlying from the sender and credit the beneficiary with new imUSD + */ + function _deposit( + uint256 _underlying, + address _beneficiary, + bool _collectInterest + ) internal returns (uint256 creditsIssued) { + require(_underlying > 0, "Must deposit something"); + require(_beneficiary != address(0), "Invalid beneficiary address"); + + // Collect recent interest generated by basket and update exchange rate + IERC20 mAsset = underlying; + if (_collectInterest) { + ISavingsManager(_savingsManager()).collectAndDistributeInterest(address(mAsset)); + } + + // Transfer tokens from sender to here + require(mAsset.transferFrom(msg.sender, address(this), _underlying), "Must receive tokens"); + + // Calc how many credits they receive based on currentRatio + (creditsIssued, ) = _underlyingToCredits(_underlying); + + // add credits to ERC20 balances + _mint(_beneficiary, creditsIssued); + + emit SavingsDeposited(_beneficiary, _underlying, creditsIssued); + } + + /*************************************** + REDEEM + ****************************************/ + + // Deprecated in favour of redeemCredits + // Maintaining backwards compatibility, this fn minimics the old redeem fn, in which + // credits are redeemed but the interest from the underlying is not collected. + function redeem(uint256 _credits) external override returns (uint256 massetReturned) { + require(_credits > 0, "Must withdraw something"); + + (, uint256 payout) = _redeem(_credits, true, true); + + // Collect recent interest generated by basket and update exchange rate + if (automateInterestCollection) { + ISavingsManager(_savingsManager()).collectAndDistributeInterest(address(underlying)); + } + + return payout; + } + + /** + * @dev Redeem specific number of the senders "credits" in exchange for underlying. + * Payout amount is calculated as a ratio of credits and exchange rate: + * payout = credits * exchangeRate + * @param _credits Amount of credits to redeem + * @return massetReturned Units of underlying mAsset paid out + */ + function redeemCredits(uint256 _credits) external override returns (uint256 massetReturned) { + require(_credits > 0, "Must withdraw something"); + + // Collect recent interest generated by basket and update exchange rate + if (automateInterestCollection) { + ISavingsManager(_savingsManager()).collectAndDistributeInterest(address(underlying)); + } + + (, uint256 payout) = _redeem(_credits, true, true); + + return payout; + } + + /** + * @dev Redeem credits into a specific amount of underlying. + * Credits needed to burn is calculated using: + * credits = underlying / exchangeRate + * @param _underlying Amount of underlying to redeem + * @return creditsBurned Units of credits burned from sender + */ + function redeemUnderlying(uint256 _underlying) + external + override + returns (uint256 creditsBurned) + { + require(_underlying > 0, "Must withdraw something"); + + // Collect recent interest generated by basket and update exchange rate + if (automateInterestCollection) { + ISavingsManager(_savingsManager()).collectAndDistributeInterest(address(underlying)); + } + + // Ensure that the payout was sufficient + (uint256 credits, uint256 massetReturned) = _redeem(_underlying, false, true); + require(massetReturned == _underlying, "Invalid output"); + + return credits; + } + + /** + * @notice Redeem credits into a specific amount of underlying, unwrap + * into a selected output asset, and send to a beneficiary + * Credits needed to burn is calculated using: + * credits = underlying / exchangeRate + * @param _amount Units to redeem (either underlying or credit amount). + * @param _isCreditAmt `true` if `amount` is in credits. eg imUSD. `false` if `amount` is in underlying. eg mUSD. + * @param _minAmountOut Minimum amount of `output` tokens to unwrap for. This is to the same decimal places as the `output` token. + * @param _output Asset to receive in exchange for the redeemed mAssets. This can be a bAsset or a fAsset. For example: + - bAssets (USDC, DAI, sUSD or USDT) or fAssets (GUSD, BUSD, alUSD, FEI or RAI) for mainnet imUSD Vault. + - bAssets (USDC, DAI or USDT) or fAsset FRAX for Polygon imUSD Vault. + - bAssets (WBTC, sBTC or renBTC) or fAssets (HBTC or TBTCV2) for mainnet imBTC Vault. + * @param _beneficiary Address to send `output` tokens to. + * @param _router mAsset address if the output is a bAsset. Feeder Pool address if the output is a fAsset. + * @param _isBassetOut `true` if `output` is a bAsset. `false` if `output` is a fAsset. + * @return creditsBurned Units of credits burned from sender. eg imUSD or imBTC. + * @return massetReturned Units of the underlying mAssets that were redeemed or swapped for the output tokens. eg mUSD or mBTC. + * @return outputQuantity Units of `output` tokens sent to the beneficiary. + */ + function redeemAndUnwrap( + uint256 _amount, + bool _isCreditAmt, + uint256 _minAmountOut, + address _output, + address _beneficiary, + address _router, + bool _isBassetOut + ) + external + override + returns ( + uint256 creditsBurned, + uint256 massetReturned, + uint256 outputQuantity + ) + { + require(_amount > 0, "Must withdraw something"); + require(_output != address(0), "Output address is zero"); + require(_beneficiary != address(0), "Beneficiary address is zero"); + require(_router != address(0), "Router address is zero"); + + // Collect recent interest generated by basket and update exchange rate + if (automateInterestCollection) { + ISavingsManager(_savingsManager()).collectAndDistributeInterest(address(underlying)); + } + + // Ensure that the payout was sufficient + (creditsBurned, massetReturned) = _redeem(_amount, _isCreditAmt, false); + require( + _isCreditAmt ? creditsBurned == _amount : massetReturned == _amount, + "Invalid output" + ); + + // Approve wrapper to spend contract's underlying; just for this tx + underlying.approve(unwrapper, massetReturned); + + // Unwrap the underlying into `output` and transfer to `beneficiary` + outputQuantity = IUnwrapper(unwrapper).unwrapAndSend( + _isBassetOut, + _router, + address(underlying), + _output, + massetReturned, + _minAmountOut, + _beneficiary + ); + } + + /** + * @dev Internally burn the credits and send the underlying to msg.sender + */ + function _redeem( + uint256 _amt, + bool _isCreditAmt, + bool _transferUnderlying + ) internal returns (uint256 creditsBurned, uint256 massetReturned) { + // Centralise credit <> underlying calcs and minimise SLOAD count + uint256 credits_; + uint256 underlying_; + uint256 exchangeRate_; + // If the input is a credit amt, then calculate underlying payout and cache the exchangeRate + if (_isCreditAmt) { + credits_ = _amt; + (underlying_, exchangeRate_) = _creditsToUnderlying(_amt); + } + // If the input is in underlying, then calculate credits needed to burn + else { + underlying_ = _amt; + (credits_, exchangeRate_) = _underlyingToCredits(_amt); + } + + // Burn required credits from the sender FIRST + _burn(msg.sender, credits_); + // Optionally, transfer tokens from here to sender + if (_transferUnderlying) { + require(underlying.transfer(msg.sender, underlying_), "Must send tokens"); + } + + // If this withdrawal pushes the portion of stored collateral in the `connector` over a certain + // threshold (fraction + 20%), then this should trigger a _poke on the connector. This is to avoid + // a situation in which there is a rush on withdrawals for some reason, causing the connector + // balance to go up and thus having too large an exposure. + CachedData memory cachedData = _cacheData(); + ConnectorStatus memory status = _getConnectorStatus(cachedData, exchangeRate_); + if (status.inConnector > status.limit) { + _poke(cachedData, false); + } + + emit CreditsRedeemed(msg.sender, credits_, underlying_); + + return (credits_, underlying_); + } + + struct ConnectorStatus { + // Limit is the max amount of units allowed in the connector + uint256 limit; + // Derived balance of the connector + uint256 inConnector; + } + + /** + * @dev Derives the units of collateral held in the connector + * @param _data Struct containing data on balances + * @param _exchangeRate Current system exchange rate + * @return status Contains max amount of assets allowed in connector + */ + function _getConnectorStatus(CachedData memory _data, uint256 _exchangeRate) + internal + pure + returns (ConnectorStatus memory) + { + // Total units of underlying collateralised + uint256 totalCollat = _data.totalCredits.mulTruncate(_exchangeRate); + // Max amount of underlying that can be held in the connector + uint256 limit = totalCollat.mulTruncate(_data.fraction + 2e17); + // Derives amount of underlying present in the connector + uint256 inConnector = _data.rawBalance >= totalCollat ? 0 : totalCollat - _data.rawBalance; + + return ConnectorStatus(limit, inConnector); + } + + /*************************************** + YIELD - E + ****************************************/ + + /** @dev Modifier allowing only the designated poker to execute the fn */ + modifier onlyPoker() { + require(msg.sender == poker, "Only poker can execute"); + _; + } + + /** + * @dev External poke function allows for the redistribution of collateral between here and the + * current connector, setting the ratio back to the defined optimal. + */ + function poke() external onlyPoker { + CachedData memory cachedData = _cacheData(); + _poke(cachedData, false); + } + + /** + * @dev Governance action to set the address of a new poker + * @param _newPoker Address of the new poker + */ + function setPoker(address _newPoker) external onlyGovernor { + require(_newPoker != address(0) && _newPoker != poker, "Invalid poker"); + + poker = _newPoker; + + emit PokerUpdated(_newPoker); + } + + /** + * @dev Governance action to set the percentage of assets that should be held + * in the connector. + * @param _fraction Percentage of assets that should be held there (where 20% == 2e17) + */ + function setFraction(uint256 _fraction) external onlyGovernor { + require(_fraction <= 5e17, "Fraction must be <= 50%"); + + fraction = _fraction; + + CachedData memory cachedData = _cacheData(); + _poke(cachedData, true); + + emit FractionUpdated(_fraction); + } + + /** + * @dev Governance action to set the address of a new connector, and move funds (if any) across. + * @param _newConnector Address of the new connector + */ + function setConnector(address _newConnector) external onlyGovernor { + // Withdraw all from previous by setting target = 0 + CachedData memory cachedData = _cacheData(); + cachedData.fraction = 0; + _poke(cachedData, true); + + // Set new connector + CachedData memory cachedDataNew = _cacheData(); + connector = IConnector(_newConnector); + _poke(cachedDataNew, true); + + emit ConnectorUpdated(_newConnector); + } + + /** + * @dev Governance action to perform an emergency withdraw of the assets in the connector, + * should it be the case that some or all of the liquidity is trapped in. This causes the total + * collateral in the system to go down, causing a hard refresh. + */ + function emergencyWithdraw(uint256 _withdrawAmount) external onlyGovernor { + // withdraw _withdrawAmount from connection + connector.withdraw(_withdrawAmount); + + // reset the connector + connector = IConnector(address(0)); + emit ConnectorUpdated(address(0)); + + // set fraction to 0 + fraction = 0; + emit FractionUpdated(0); + + // check total collateralisation of credits + CachedData memory data = _cacheData(); + // use rawBalance as the remaining liquidity in the connector is now written off + _refreshExchangeRate(data.rawBalance, data.totalCredits, true); + + emit EmergencyUpdate(); + } + + /*************************************** + YIELD - I + ****************************************/ + + /** @dev Internal poke function to keep the balance between connector and raw balance healthy */ + function _poke(CachedData memory _data, bool _ignoreCadence) internal { + require(_data.totalCredits > 0, "Must have something to poke"); + + // 1. Verify that poke cadence is valid, unless this is a manual action by governance + uint256 currentTime = uint256(block.timestamp); + uint256 timeSinceLastPoke = currentTime - lastPoke; + require(_ignoreCadence || timeSinceLastPoke > POKE_CADENCE, "Not enough time elapsed"); + lastPoke = currentTime; + + // If there is a connector, check the balance and settle to the specified fraction % + IConnector connector_ = connector; + if (address(connector_) != address(0)) { + // 2. Check and verify new connector balance + uint256 lastBalance_ = lastBalance; + uint256 connectorBalance = connector_.checkBalance(); + // Always expect the collateral in the connector to increase in value + require(connectorBalance >= lastBalance_, "Invalid yield"); + if (connectorBalance > 0) { + // Validate the collection by ensuring that the APY is not ridiculous + YieldValidator.validateCollection( + connectorBalance, + connectorBalance - lastBalance_, + timeSinceLastPoke, + MAX_APY, + 1e15 + ); + } + + // 3. Level the assets to Fraction (connector) & 100-fraction (raw) + uint256 sum = _data.rawBalance + connectorBalance; + uint256 ideal = sum.mulTruncate(_data.fraction); + // If there is not enough mAsset in the connector, then deposit + if (ideal > connectorBalance) { + uint256 deposit = ideal - connectorBalance; + underlying.approve(address(connector_), deposit); + connector_.deposit(deposit); + } + // Else withdraw, if there is too much mAsset in the connector + else if (connectorBalance > ideal) { + // If fraction == 0, then withdraw everything + if (ideal == 0) { + connector_.withdrawAll(); + sum = IERC20(underlying).balanceOf(address(this)); + } else { + connector_.withdraw(connectorBalance - ideal); + } + } + // Else ideal == connectorBalance (e.g. 0), do nothing + require(connector_.checkBalance() >= ideal, "Enforce system invariant"); + + // 4i. Refresh exchange rate and emit event + lastBalance = ideal; + _refreshExchangeRate(sum, _data.totalCredits, false); + emit Poked(lastBalance_, ideal, connectorBalance - lastBalance_); + } else { + // 4ii. Refresh exchange rate and emit event + lastBalance = 0; + _refreshExchangeRate(_data.rawBalance, _data.totalCredits, false); + emit PokedRaw(); + } + } + + /** + * @dev Internal fn to refresh the exchange rate, based on the sum of collateral and the number of credits + * @param _realSum Sum of collateral held by the contract + * @param _totalCredits Total number of credits in the system + * @param _ignoreValidation This is for use in the emergency situation, and ignores a decreasing exchangeRate + */ + function _refreshExchangeRate( + uint256 _realSum, + uint256 _totalCredits, + bool _ignoreValidation + ) internal { + // Based on the current exchange rate, how much underlying is collateralised? + (uint256 totalCredited, ) = _creditsToUnderlying(_totalCredits); + + // Require the amount of capital held to be greater than the previously credited units + require(_ignoreValidation || _realSum >= totalCredited, "ExchangeRate must increase"); + // Work out the new exchange rate based on the current capital + uint256 newExchangeRate = _calcExchangeRate(_realSum, _totalCredits); + exchangeRate = newExchangeRate; + + emit ExchangeRateUpdated( + newExchangeRate, + _realSum > totalCredited ? _realSum - totalCredited : 0 + ); + } + + /*************************************** + VIEW - I + ****************************************/ + + struct CachedData { + // SLOAD from 'fraction' + uint256 fraction; + // ERC20 balance of underlying, held by this contract + // underlying.balanceOf(address(this)) + uint256 rawBalance; + // totalSupply() + uint256 totalCredits; + } + + /** + * @dev Retrieves generic data to avoid duplicate SLOADs + */ + function _cacheData() internal view returns (CachedData memory) { + uint256 balance = underlying.balanceOf(address(this)); + return CachedData(fraction, balance, totalSupply()); + } + + /** + * @dev Converts masset amount into credits based on exchange rate + * c = (masset / exchangeRate) + 1 + */ + function _underlyingToCredits(uint256 _underlying) + internal + view + returns (uint256 credits, uint256 exchangeRate_) + { + // e.g. (1e20 * 1e18) / 1e18 = 1e20 + // e.g. (1e20 * 1e18) / 14e17 = 7.1429e19 + // e.g. 1 * 1e18 / 1e17 + 1 = 11 => 11 * 1e17 / 1e18 = 1.1e18 / 1e18 = 1 + exchangeRate_ = exchangeRate; + credits = _underlying.divPrecisely(exchangeRate_) + 1; + } + + /** + * @dev Works out a new exchange rate, given an amount of collateral and total credits + * e = underlying / (credits-1) + */ + function _calcExchangeRate(uint256 _totalCollateral, uint256 _totalCredits) + internal + pure + returns (uint256 _exchangeRate) + { + _exchangeRate = _totalCollateral.divPrecisely(_totalCredits - 1); + } + + /** + * @dev Converts credit amount into masset based on exchange rate + * m = credits * exchangeRate + */ + function _creditsToUnderlying(uint256 _credits) + internal + view + returns (uint256 underlyingAmount, uint256 exchangeRate_) + { + // e.g. (1e20 * 1e18) / 1e18 = 1e20 + // e.g. (1e20 * 14e17) / 1e18 = 1.4e20 + exchangeRate_ = exchangeRate; + underlyingAmount = _credits.mulTruncate(exchangeRate_); + } +} diff --git a/contracts/savings/SavingsContract.sol b/contracts/savings/SavingsContract.sol index 1f1f0eab..8a586c09 100644 --- a/contracts/savings/SavingsContract.sol +++ b/contracts/savings/SavingsContract.sol @@ -7,6 +7,7 @@ import { ISavingsManager } from "../interfaces/ISavingsManager.sol"; // Internal import { ISavingsContractV3 } from "../interfaces/ISavingsContract.sol"; import { IUnwrapper } from "../interfaces/IUnwrapper.sol"; +import { IERC4626Vault } from "../interfaces/IERC4626Vault.sol"; import { InitializableToken } from "../shared/InitializableToken.sol"; import { ImmutableModule } from "../shared/ImmutableModule.sol"; import { IConnector } from "./peripheral/IConnector.sol"; @@ -25,9 +26,15 @@ import { YieldValidator } from "../shared/YieldValidator.sol"; * the value of the Savers "credits" (ERC20) relative to the amount of additional * underlying collateral that has been deposited into this contract ("interest") * @dev VERSION: 2.2 - * DATE: 2021-02-08 + * DATE: 2021-04-08 */ -contract SavingsContract is ISavingsContractV3, Initializable, InitializableToken, ImmutablexModule { +contract SavingsContract is + ISavingsContractV3, + IERC4626Vault, + Initializable, + InitializableToken, + ImmutableModule +{ using StableMath for uint256; using SafeERC20 for IERC20; @@ -81,7 +88,6 @@ contract SavingsContract is ISavingsContractV3, Initializable, InitializableToke // Max APY generated on the capital in the connector uint256 private constant MAX_APY = 4e18; uint256 private constant SECONDS_IN_YEAR = 365 days; - uint256 private constant MAX_INT = 2**256 - 1; // Proxy contract for easy redemption address public immutable unwrapper; @@ -123,7 +129,6 @@ contract SavingsContract is ISavingsContractV3, Initializable, InitializableToke ****************************************/ /** - * @dev Deprecated in favour of IERC4626Vault.assetsOf(addresss depositor) * @notice Returns the underlying balance of a given user * @param _user Address of the user to check * @return balance Units of underlying owned by the user. eg mUSD or mBTC @@ -134,6 +139,7 @@ contract SavingsContract is ISavingsContractV3, Initializable, InitializableToke /** * @notice Converts a given underlying amount into credits. eg mUSD to imUSD. + * @dev see IERC4626Vault.convertToShares() * @param _underlying Units of underlying. eg mUSD or mBTC. * @return credits Units of crefit. eg imUSD or imBTC */ @@ -148,6 +154,7 @@ contract SavingsContract is ISavingsContractV3, Initializable, InitializableToke /** * @notice Converts a given credit amount into underlying. eg imUSD to mUSD + * @dev see IERC4626Vault.convertToAssets(address) * @param _credits Units of credits. eg imUSD or imBTC * @return amount Units of underlying. eg mUSD or mBTC. */ @@ -156,7 +163,7 @@ contract SavingsContract is ISavingsContractV3, Initializable, InitializableToke } /** - * @dev Deprecated in favour of `balanceOf(address)` + * @dev see IERC4626Vault.balanceOf(address) * Maintained for backwards compatibility * Returns the credit balance of a given user **/ @@ -176,7 +183,23 @@ contract SavingsContract is ISavingsContractV3, Initializable, InitializableToke * @param _amount Units of underlying to add to the savings vault */ function depositInterest(uint256 _amount) external override onlySavingsManager { - _mint(_amount, msg.sender, false); + require(_amount > 0, "Must deposit something"); + + // Transfer the interest from sender to here + require(underlying.transferFrom(msg.sender, address(this), _amount), "Must receive tokens"); + + // Calc new exchange rate, protect against initialisation case + uint256 totalCredits = totalSupply(); + if (totalCredits > 0) { + // new exchange rate is relationship between _totalCredits & totalSavings + // _totalCredits * exchangeRate = totalSavings + // exchangeRate = totalSavings/_totalCredits + (uint256 totalCollat, ) = _creditsToUnderlying(totalCredits); + uint256 newExchangeRate = _calcExchangeRate(totalCollat + _amount, totalCredits); + exchangeRate = newExchangeRate; + + emit ExchangeRateUpdated(newExchangeRate, _amount); + } } /** @notice Enable or disable the automation of fee collection during deposit process */ @@ -204,8 +227,7 @@ contract SavingsContract is ISavingsContractV3, Initializable, InitializableToke } /** - * @dev Deprecated in favour of IERC4626Vault.deposit(uint256 assets, address receiver) - * + * @dev see IERC4626Vault.deposit(uint256 assets, address receiver) * @notice Deposit the senders savings to the vault, and credit them internally with "credits". * Credit amount is calculated as a ratio of deposit amount and exchange rate: * credits = underlying / exchangeRate @@ -218,8 +240,7 @@ contract SavingsContract is ISavingsContractV3, Initializable, InitializableToke } /** - * @dev Deprecated in favour of IERC4626Vault.deposit(uint256 assets, address receiver) - * + * @dev see IERC4626Vault.deposit(uint256 assets, address receiver) * @notice Deposit the senders savings to the vault, and credit them internally with "credits". * Credit amount is calculated as a ratio of deposit amount and exchange rate: * credits = underlying / exchangeRate @@ -237,8 +258,7 @@ contract SavingsContract is ISavingsContractV3, Initializable, InitializableToke } /** - * @dev Deprecated in favour of IERC4626Vault.deposit(uint256 assets, address receiver, address _referrer) - * + * @dev see IERC4626Vault.deposit(uint256 assets, address receiver, address _referrer) * @notice Overloaded `depositSavings` method with an optional referrer address. * @param _underlying Units of underlying to deposit into savings vault. eg mUSD or mBTC * @param _beneficiary Address to the new credits will be issued to. @@ -262,26 +282,8 @@ contract SavingsContract is ISavingsContractV3, Initializable, InitializableToke address _beneficiary, bool _collectInterest ) internal returns (uint256 creditsIssued) { - require(_underlying > 0, "Must deposit something"); - require(_beneficiary != address(0), "Invalid beneficiary address"); - - // Collect recent interest generated by basket and update exchange rate - IERC20 mAsset = underlying; - if (_collectInterest) { - ISavingsManager(_savingsManager()).collectAndDistributeInterest(address(mAsset)); - } - - // Transfer tokens from sender to here - require(mAsset.transferFrom(msg.sender, address(this), _underlying), "Must receive tokens"); - - // Calc how many credits they receive based on currentRatio - (creditsIssued, ) = _underlyingToCredits(_underlying); - - // add credits to ERC20 balances - _mint(_beneficiary, creditsIssued); - + creditsIssued = _transferAndMint(_underlying, _beneficiary, _collectInterest); emit SavingsDeposited(_beneficiary, _underlying, creditsIssued); - emit Deposit(msg.sender, _beneficiary, _underlying); } /*************************************** @@ -306,6 +308,7 @@ contract SavingsContract is ISavingsContractV3, Initializable, InitializableToke } /** + * @dev see IERC4626.redeem(uint256 shares,address receiver,address owner) * @notice Redeem specific number of the senders "credits" in exchange for underlying. * Payout amount is calculated as a ratio of credits and exchange rate: * payout = credits * exchangeRate @@ -315,7 +318,10 @@ contract SavingsContract is ISavingsContractV3, Initializable, InitializableToke function redeemCredits(uint256 _credits) external override returns (uint256 massetReturned) { require(_credits > 0, "Must withdraw something"); - _beforeRedeem(); + // Collect recent interest generated by basket and update exchange rate + if (automateInterestCollection) { + ISavingsManager(_savingsManager()).collectAndDistributeInterest(address(underlying)); + } (, uint256 payout) = _redeem(_credits, true, true); @@ -323,6 +329,7 @@ contract SavingsContract is ISavingsContractV3, Initializable, InitializableToke } /** + * @dev see IERC4626.redeem(uint256 shares,address receiver,address owner) * @notice Redeem credits into a specific amount of underlying. * Credits needed to burn is calculated using: * credits = underlying / exchangeRate @@ -334,7 +341,18 @@ contract SavingsContract is ISavingsContractV3, Initializable, InitializableToke override returns (uint256 creditsBurned) { - return _withdraw(_underlying, msg.sender, msg.sender); + require(_underlying > 0, "Must withdraw something"); + + // Collect recent interest generated by basket and update exchange rate + if (automateInterestCollection) { + ISavingsManager(_savingsManager()).collectAndDistributeInterest(address(underlying)); + } + + // Ensure that the payout was sufficient + (uint256 credits, uint256 massetReturned) = _redeem(_underlying, false, true); + require(massetReturned == _underlying, "Invalid output"); + + return credits; } /** @@ -378,7 +396,10 @@ contract SavingsContract is ISavingsContractV3, Initializable, InitializableToke require(_beneficiary != address(0), "Beneficiary address is zero"); require(_router != address(0), "Router address is zero"); - _beforeRedeem(); + // Collect recent interest generated by basket and update exchange rate + if (automateInterestCollection) { + ISavingsManager(_savingsManager()).collectAndDistributeInterest(address(underlying)); + } // Ensure that the payout was sufficient (creditsBurned, massetReturned) = _redeem(_amount, _isCreditAmt, false); @@ -402,33 +423,11 @@ contract SavingsContract is ISavingsContractV3, Initializable, InitializableToke ); } - /** - * @dev Internally call before redeeming credits. It collects recent interest generated by basket and update exchange rate. - */ - function _beforeRedeem() internal { - if (automateInterestCollection) { - ISavingsManager(_savingsManager()).collectAndDistributeInterest(address(underlying)); - } - } - - /** - * @dev Internally burn the credits and optionally send the underlying to msg.sender - */ - function _redeem( - uint256 _amt, - bool _isCreditAmt, - bool _transferUnderlying - ) internal returns (uint256 creditsBurned, uint256 massetReturned) { - return _redeem(_amt, msg.sender, msg.sender, _isCreditAmt, _transferUnderlying); - } - /** * @dev Internally burn the credits and optionally send the underlying to msg.sender */ function _redeem( uint256 _amt, - address receiver, - address owner, bool _isCreditAmt, bool _transferUnderlying ) internal returns (uint256 creditsBurned, uint256 massetReturned) { @@ -447,24 +446,14 @@ contract SavingsContract is ISavingsContractV3, Initializable, InitializableToke (credits_, exchangeRate_) = _underlyingToCredits(_amt); } - // Burn required credits from the sender FIRST - _burn(msg.sender, credits_); - - // Optionally, transfer tokens from here to sender - if (_transferUnderlying) { - underlying.safeTransfer(receiver, underlying_); - emit Withdraw(receiver, owner, underlying_, credits_); - } - - // If this withdrawal pushes the portion of stored collateral in the `connector` over a certain - // threshold (fraction + 20%), then this should trigger a _poke on the connector. This is to avoid - // a situation in which there is a rush on withdrawals for some reason, causing the connector - // balance to go up and thus having too large an exposure. - CachedData memory cachedData = _cacheData(); - ConnectorStatus memory status = _getConnectorStatus(cachedData, exchangeRate_); - if (status.inConnector > status.limit) { - _poke(cachedData, false); - } + _burnTransfer( + underlying_, + credits_, + msg.sender, + msg.sender, + exchangeRate_, + _transferUnderlying + ); emit CreditsRedeemed(msg.sender, credits_, underlying_); @@ -761,8 +750,7 @@ contract SavingsContract is ISavingsContractV3, Initializable, InitializableToke } /** - * @notice It should include any compounding that occurs from yield. It must be inclusive of any fees that are charged against assets in the Vault. It must not revert. - * + * @notice The address of the underlying token used for the Vault uses for accounting, depositing, and withdrawing. * Returns the total amount of the underlying asset that is “managed” by Vault. */ function totalAssets() external view override returns (uint256 totalManagedAssets) { @@ -770,106 +758,108 @@ contract SavingsContract is ISavingsContractV3, Initializable, InitializableToke } /** - * @notice It must be inclusive of any fees that are charged against assets in the Vault. - * - * Returns the current exchange rate of shares to assets, quoted per unit share (share unit is 10 ** Vault.decimals()). + * @notice The amount of shares that the Vault would exchange for the amount of assets provided, in an ideal scenario where all the conditions are met. + * @param assets The amount of underlying assets to be convert to vault shares. + * @return shares The amount of vault shares converted from the underlying assets. */ - function assetsPerShare() external view override returns (uint256 assetsPerUnitShare) { - return exchangeRate; + function convertToShares(uint256 assets) external view override returns (uint256 shares) { + (shares, ) = _underlyingToCredits(assets); } /** - * @notice Returns the underlying assets balance of a given depositor - * @param depositor Address of the user to check - * @return assets Units of underlying assets owned by the depositor. eg mUSD or mBTC - - * Returns the total number of underlying assets that depositor’s shares represent. + * @notice The amount of assets that the Vault would exchange for the amount of shares provided, in an ideal scenario where all the conditions are met. + * @param shares The amount of vault shares to be converted to the underlying assets. + * @return assets The amount of underlying assets converted from the vault shares. */ - function assetsOf(address depositor) external view override returns (uint256 assets) { - (assets, ) = _creditsToUnderlying(balanceOf(depositor)); + function convertToAssets(uint256 shares) external view override returns (uint256 assets) { + (assets, ) = _creditsToUnderlying(shares); } /** - * @notice It must return a limited value if caller is subject to some deposit limit. must return 2 ** 256 - 1 if there is no limit on the maximum amount of assets that may be deposited. - * - * Returns the total number of underlying assets that caller can be deposit. + * @notice The maximum number of underlying assets that caller can deposit. + * @param caller Account that the assets will be transferred from. + * @return maxAssets The maximum amount of underlying assets the caller can deposit. */ - function maxDeposit(address) external pure override returns (uint256 maxAssets) { - return MAX_INT; + function maxDeposit(address caller) external view override returns (uint256 maxAssets) { + // TODO - ASK NICK WHAT he thinks about this. + maxAssets = IERC20(underlying).balanceOf(caller); } /** - * @notice It must return the exact amount of Vault shares that would be minted if the caller were to deposit a given exact amount of underlying assets using the deposit method. - * - * It simulate the effects of their deposit at the current block, given current on-chain conditions. - * Returns the amount of shares. + * @notice Allows an on-chain or off-chain user to simulate the effects of their deposit at the current block, given current on-chain conditions. + * @param assets The amount of underlying assets to be transferred. + * @return shares The amount of vault shares that will be minted. */ function previewDeposit(uint256 assets) external view override returns (uint256 shares) { + require(assets > 0, "Must deposit something"); (shares, ) = _underlyingToCredits(assets); } /** - * @notice Deposit the senders savings to the vault, and credit them internally with "credits". + * @notice Mint vault shares to receiver by transferring exact amount of underlying asset tokens from the caller. * Credit amount is calculated as a ratio of deposit amount and exchange rate: * credits = underlying / exchangeRate * We will first update the internal exchange rate by collecting any interest generated on the underlying. * Emits a {Deposit} event. * @param assets Units of underlying to deposit into savings vault. eg mUSD or mBTC * @param receiver The address to receive the Vault shares. - * @return shares Units of credits issued. eg imUSD or imBTC + * @return shares Units of credits issued. eg imUSD or imBTC */ function deposit(uint256 assets, address receiver) external override returns (uint256 shares) { - return _deposit(assets, receiver, true); + shares = _transferAndMint(assets, receiver, true); + emit Deposit(msg.sender, receiver, assets, shares); } /** * * @notice Overloaded `deposit` method with an optional referrer address. - * @param assets Units of underlying to deposit into savings vault. eg mUSD or mBTC - * @param receiver Address to the new credits will be issued to. - * @param referrer Referrer address for this deposit. + * @param assets Units of underlying to deposit into savings vault. eg mUSD or mBTC + * @param receiver Address to the new credits will be issued to. + * @param referrer Referrer address for this deposit. * @return shares Units of credits issued. eg imUSD or imBTC */ function deposit( uint256 assets, address receiver, address referrer - ) external override returns (uint256 shares) { + ) external returns (uint256 shares) { + shares = _transferAndMint(assets, receiver, true); emit Referral(referrer, receiver, assets); - return _deposit(assets, receiver, true); + emit Deposit(msg.sender, receiver, assets, shares); } /** - * @notice must return a limited value if caller is subject to some deposit limit. must return 2 ** 256 - 1 if there is no limit on the maximum amount of shares that may be minted - * - * Returns Total number of underlying shares that caller can be mint. + * @notice The maximum number of vault shares that caller can mint. + * @param caller Account that the underlying assets will be transferred from. + * @return maxShares The maximum amount of vault shares the caller can mint. */ function maxMint(address caller) external view override returns (uint256 maxShares) { maxShares = balanceOf(caller); - return maxShares; } /** * @notice Allows an on-chain or off-chain user to simulate the effects of their mint at the current block, given current on-chain conditions. - * - * Returns Total number of underlying shares to be minted. + * @param shares The amount of vault shares to be minted. + * @return assets The amount of underlying assests that will be transferred from the caller. */ function previewMint(uint256 shares) external view override returns (uint256 assets) { (assets, ) = _creditsToUnderlying(shares); - // TODO - review when Nothing deposit yet return assets; } /** - * Mints exactly shares Vault shares to receiver by depositing amount of underlying tokens. - * - * Returns Total number of underlying shares that caller mint. + * @notice Mint exact amount of vault shares to the receiver by transferring enough underlying asset tokens from the caller. + * @param shares The amount of vault shares to be minted. + * @param receiver The account the vault shares will be minted to. + * @return assets The amount of underlying assets that were transferred from the caller. * Emits a {Deposit} event. */ function mint(uint256 shares, address receiver) external override returns (uint256 assets) { - assets = _mint(shares, receiver, true); - return assets; + (assets, ) = _creditsToUnderlying(shares); + _transferAndMint(assets, receiver, true); + emit Deposit(msg.sender, receiver, assets, shares); } + /** * * Returns Total number of underlying assets that caller can withdraw. @@ -899,7 +889,15 @@ contract SavingsContract is ISavingsContractV3, Initializable, InitializableToke address receiver, address owner ) external override returns (uint256 shares) { - return _withdraw(assets, receiver, owner); + require(assets > 0, "Must withdraw something"); + uint256 _exchangeRate; + if (automateInterestCollection) { + ISavingsManager(_savingsManager()).collectAndDistributeInterest(address(underlying)); + } + (shares, _exchangeRate) = _underlyingToCredits(assets); + + _burnTransfer(assets, shares, receiver, owner, _exchangeRate, true); //transferAssets=true + emit Withdraw(msg.sender, receiver, owner, assets, shares); } /** @@ -933,63 +931,82 @@ contract SavingsContract is ISavingsContractV3, Initializable, InitializableToke address receiver, address owner ) external override returns (uint256 assets) { - require(shares > 0, "Must withdraw something"); - - _beforeRedeem(); - - (, assets) = _redeem(shares, receiver, owner, true, true); + require(assets > 0, "Must withdraw something"); + uint256 _exchangeRate; + if (automateInterestCollection) { + ISavingsManager(_savingsManager()).collectAndDistributeInterest(address(underlying)); + } + (assets, _exchangeRate) = _creditsToUnderlying(shares); - return assets; + _burnTransfer(assets, shares, receiver, owner, _exchangeRate, true); //transferAssets=true + emit Withdraw(msg.sender, receiver, owner, assets, shares); } - function _mint( - uint256 _amount, + + /*/////////////////////////////////////////////////////////////// + INTERNAL DEPOSIT/MINT + //////////////////////////////////////////////////////////////*/ + function _transferAndMint( + uint256 assets, address receiver, - bool _isCreditAmt - ) internal returns (uint256 assets) { - require(_amount > 0, "Must deposit something"); + bool _collectInterest + ) internal returns (uint256 shares) { + require(assets > 0, "Must deposit something"); + require(receiver != address(0), "Invalid beneficiary address"); - if (_isCreditAmt) { - (assets, ) = _creditsToUnderlying(_amount); - } else { - assets = _amount; + // Collect recent interest generated by basket and update exchange rate + IERC20 mAsset = underlying; + if (_collectInterest) { + ISavingsManager(_savingsManager()).collectAndDistributeInterest(address(mAsset)); } - // Transfer the interest from sender to here - require(underlying.transferFrom(msg.sender, address(this), assets), "Must receive tokens"); - if (_isCreditAmt) { - _mint(receiver, assets); - emit Deposit(msg.sender, receiver, assets); - } - // Calc new exchange rate, protect against initialisation case - uint256 totalCredits = totalSupply(); - if (totalCredits > 0) { - // new exchange rate is relationship between _totalCredits & totalSavings - // _totalCredits * exchangeRate = totalSavings - // exchangeRate = totalSavings/_totalCredits - (uint256 totalCollat, ) = _creditsToUnderlying(totalCredits); - uint256 newExchangeRate = _calcExchangeRate(totalCollat + assets, totalCredits); - exchangeRate = newExchangeRate; + // Transfer tokens from sender to here + require(mAsset.transferFrom(msg.sender, address(this), assets), "Must receive tokens"); - emit ExchangeRateUpdated(newExchangeRate, assets); - } + // Calc how many credits they receive based on currentRatio + (shares, ) = _underlyingToCredits(assets); - return (assets); + // add credits to ERC20 balances + _mint(receiver, shares); } - function _withdraw( + /*/////////////////////////////////////////////////////////////// + INTERNAL WITHDRAW/REDEEM + //////////////////////////////////////////////////////////////*/ + + function _burnTransfer( uint256 assets, + uint256 shares, address receiver, - address owner - ) internal returns (uint256 shares) { - require(assets > 0, "Must withdraw something"); + address owner, + uint256 _exchangeRate, + bool transferAssets + ) internal { + // If caller is not the owner of the shares + uint256 allowed = allowance(owner, msg.sender); + // TODO - RISK ask nick about , just compare allowance instead of approving. + if (msg.sender != owner && allowed != type(uint256).max) { + // _approve(owner, msg.sender, allowed - shares); + require(assets <= allowed, "!allowance"); + } + // _beforeWithdrawHook(assets, shares, owner); - _beforeRedeem(); - // if (msg.sender != owner && allowed != type(uint256).max) allowance[owner][msg.sender] = allowed - shares; + // Burn required shares from the sender FIRST + _burn(owner, shares); - // Ensure that the payout was sufficient - (uint256 credits, uint256 massetReturned) = _redeem(assets, receiver, owner, false, true); - require(massetReturned == assets, "Invalid output"); + // Optionally, transfer tokens from here to sender + if (transferAssets) { + // underlying.safeTransfer(receiver, assets); + require(underlying.transfer(msg.sender, assets), "Must send tokens"); + } - return credits; + // If this withdrawal pushes the portion of stored collateral in the `connector` over a certain + // threshold (fraction + 20%), then this should trigger a _poke on the connector. This is to avoid + // a situation in which there is a rush on withdrawals for some reason, causing the connector + // balance to go up and thus having too large an exposure. + CachedData memory cachedData = _cacheData(); + ConnectorStatus memory status = _getConnectorStatus(cachedData, _exchangeRate); + if (status.inConnector > status.limit) { + _poke(cachedData, false); + } } } diff --git a/package.json b/package.json index 59912028..82c3f233 100644 --- a/package.json +++ b/package.json @@ -75,9 +75,9 @@ "pretty-quick": "^3.1.0", "sol-merger": "^3.0.1", "solc": "0.8.6", - "solhint": "^3.3.6", + "solhint": "^3.3.7", "ts-generator": "^0.1.1", - "typescript": "^4.3.5" + "typescript": "^4.6.3" }, "_moduleAliases": { "@utils": "dist/test-utils" diff --git a/tasks/save.ts b/tasks/save.ts index 6dd53b36..c2f38fb2 100644 --- a/tasks/save.ts +++ b/tasks/save.ts @@ -38,7 +38,7 @@ subtask("save-redeem", "Redeems a number of Save credits from a savings contract const amount = simpleToExactAmount(taskArgs.amount) - const tx = await save.redeem(amount) + const tx = await save["redeem(uint256)"](amount) await logTxDetails(tx, `redeem ${taskArgs.amount} ${taskArgs.masset} in Save`) }) task("save-redeem").setAction(async (_, __, runSuper) => { diff --git a/test/savings/savings-contract.spec.ts b/test/savings/savings-contract.spec.ts index 1d9dc0b4..36d6f582 100644 --- a/test/savings/savings-contract.spec.ts +++ b/test/savings/savings-contract.spec.ts @@ -560,13 +560,13 @@ describe("SavingsContract", async () => { await createNewSavingsContract() }) it("should fail when input is zero", async () => { - await expect(savingsContract.redeem(ZERO)).to.be.revertedWith("Must withdraw something") + await expect(savingsContract["redeem(uint256)"](ZERO)).to.be.revertedWith("Must withdraw something") await expect(savingsContract.redeemCredits(ZERO)).to.be.revertedWith("Must withdraw something") await expect(savingsContract.redeemUnderlying(ZERO)).to.be.revertedWith("Must withdraw something") }) it("should fail when user doesn't have credits", async () => { const amt = BN.from(10) - await expect(savingsContract.connect(sa.other.signer).redeem(amt)).to.be.revertedWith("VM Exception") + await expect(savingsContract.connect(sa.other.signer)["redeem(uint256)"](amt)).to.be.revertedWith("VM Exception") await expect(savingsContract.connect(sa.other.signer).redeemCredits(amt)).to.be.revertedWith("VM Exception") await expect(savingsContract.connect(sa.other.signer).redeemUnderlying(amt)).to.be.revertedWith("VM Exception") }) @@ -745,7 +745,7 @@ describe("SavingsContract", async () => { const redemptionAmount = simpleToExactAmount(5, 18) const balancesBefore = await getData(savingsContract, sa.default) - const tx = savingsContract.redeem(redemptionAmount) + const tx = savingsContract["redeem(uint256)"](redemptionAmount) const exchangeRate = initialExchangeRate const underlying = creditsToUnderlying(redemptionAmount, exchangeRate) await expect(tx).to.emit(savingsContract, "CreditsRedeemed").withArgs(sa.default.address, redemptionAmount, underlying) @@ -759,7 +759,7 @@ describe("SavingsContract", async () => { const creditsBefore = await savingsContract.creditBalances(sa.default.address) const mUSDBefore = await masset.balanceOf(sa.default.address) // Redeem all the credits - await savingsContract.redeem(creditsBefore) + await savingsContract["redeem(uint256)"](creditsBefore) const creditsAfter = await savingsContract.creditBalances(sa.default.address) const mUSDAfter = await masset.balanceOf(sa.default.address) @@ -1316,7 +1316,7 @@ describe("SavingsContract", async () => { await savingsContract.connect(saver3.signer)["depositSavings(uint256)"](saver3deposit) const state3 = await getData(savingsContract, saver3) // 4.0 user 1 withdraws all her credits - await savingsContract.connect(saver1.signer).redeem(state1.balances.userCredits) + await savingsContract.connect(saver1.signer)["redeem(uint256)"](state1.balances.userCredits) const state4 = await getData(savingsContract, saver1) expect(state4.balances.userCredits).eq(BN.from(0)) expect(state4.balances.totalCredits).eq(state3.balances.totalCredits.sub(state1.balances.userCredits)) diff --git a/yarn.lock b/yarn.lock index 159d4dfc..58071780 100644 --- a/yarn.lock +++ b/yarn.lock @@ -996,6 +996,13 @@ dependencies: antlr4ts "^0.5.0-alpha.4" +"@solidity-parser/parser@^0.14.1": + version "0.14.1" + resolved "https://registry.yarnpkg.com/@solidity-parser/parser/-/parser-0.14.1.tgz#179afb29f4e295a77cc141151f26b3848abc3c46" + integrity sha512-eLjj2L6AuQjBB6s/ibwCAc0DwrR5Ge+ys+wgWo+bviU7fV2nTMQhU63CGaDKXg9iTmMxwhkyoggdIR7ZGRfMgw== + dependencies: + antlr4ts "^0.5.0-alpha.4" + "@szmarczak/http-timer@^1.1.2": version "1.1.2" resolved "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-1.1.2.tgz" @@ -9651,12 +9658,12 @@ solc@^0.6.3: semver "^5.5.0" tmp "0.0.33" -solhint@^3.3.6: - version "3.3.6" - resolved "https://registry.npmjs.org/solhint/-/solhint-3.3.6.tgz" - integrity sha512-HWUxTAv2h7hx3s3hAab3ifnlwb02ZWhwFU/wSudUHqteMS3ll9c+m1FlGn9V8ztE2rf3Z82fQZA005Wv7KpcFA== +solhint@^3.3.7: + version "3.3.7" + resolved "https://registry.yarnpkg.com/solhint/-/solhint-3.3.7.tgz#b5da4fedf7a0fee954cb613b6c55a5a2b0063aa7" + integrity sha512-NjjjVmXI3ehKkb3aNtRJWw55SUVJ8HMKKodwe0HnejA+k0d2kmhw7jvpa+MCTbcEgt8IWSwx0Hu6aCo/iYOZzQ== dependencies: - "@solidity-parser/parser" "^0.13.2" + "@solidity-parser/parser" "^0.14.1" ajv "^6.6.1" antlr4 "4.7.1" ast-parents "0.0.1" @@ -10544,10 +10551,10 @@ typescript@^4.3.4: resolved "https://registry.npmjs.org/typescript/-/typescript-4.3.4.tgz" integrity sha512-uauPG7XZn9F/mo+7MrsRjyvbxFpzemRjKEZXS4AK83oP2KKOJPvb+9cO/gmnv8arWZvhnjVOXz7B49m1l0e9Ew== -typescript@^4.3.5: - version "4.3.5" - resolved "https://registry.npmjs.org/typescript/-/typescript-4.3.5.tgz" - integrity sha512-DqQgihaQ9cUrskJo9kIyW/+g0Vxsk8cDtZ52a3NGh0YNTfpUSArXSohyUGnvbPazEPLu398C0UxmKSOrPumUzA== +typescript@^4.6.3: + version "4.6.3" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.6.3.tgz#eefeafa6afdd31d725584c67a0eaba80f6fc6c6c" + integrity sha512-yNIatDa5iaofVozS/uQJEl3JRWLKKGJKh6Yaiv0GLGSuhpFJe7P3SbHZ8/yjAHRQwKRoA6YZqlfjXWmVzoVSMw== typewise-core@^1.2, typewise-core@^1.2.0: version "1.2.0" From a11332a642f9f933382ab215fb623d43167f1dc5 Mon Sep 17 00:00:00 2001 From: doncesarts Date: Thu, 7 Apr 2022 14:40:29 +0100 Subject: [PATCH 05/14] test: adds SavingContract tests for deposit and redeem --- contracts/savings/SavingsContract.sol | 20 +- test/savings/savings-contract.spec.ts | 673 ++++++++++++++++---------- test/shared/ERC4626.behaviours.ts | 173 +++++++ 3 files changed, 585 insertions(+), 281 deletions(-) create mode 100644 test/shared/ERC4626.behaviours.ts diff --git a/contracts/savings/SavingsContract.sol b/contracts/savings/SavingsContract.sol index 8a586c09..34fddf3d 100644 --- a/contracts/savings/SavingsContract.sol +++ b/contracts/savings/SavingsContract.sol @@ -777,12 +777,11 @@ contract SavingsContract is /** * @notice The maximum number of underlying assets that caller can deposit. - * @param caller Account that the assets will be transferred from. + * caller Account that the assets will be transferred from. * @return maxAssets The maximum amount of underlying assets the caller can deposit. */ - function maxDeposit(address caller) external view override returns (uint256 maxAssets) { - // TODO - ASK NICK WHAT he thinks about this. - maxAssets = IERC20(underlying).balanceOf(caller); + function maxDeposit(address /** caller **/) external pure override returns (uint256 maxAssets) { + maxAssets = type(uint256).max; } /** @@ -896,7 +895,7 @@ contract SavingsContract is } (shares, _exchangeRate) = _underlyingToCredits(assets); - _burnTransfer(assets, shares, receiver, owner, _exchangeRate, true); //transferAssets=true + _burnTransfer(assets, shares, receiver, owner, _exchangeRate, true); emit Withdraw(msg.sender, receiver, owner, assets, shares); } @@ -931,7 +930,7 @@ contract SavingsContract is address receiver, address owner ) external override returns (uint256 assets) { - require(assets > 0, "Must withdraw something"); + require(shares > 0, "Must withdraw something"); uint256 _exchangeRate; if (automateInterestCollection) { ISavingsManager(_savingsManager()).collectAndDistributeInterest(address(underlying)); @@ -983,20 +982,17 @@ contract SavingsContract is ) internal { // If caller is not the owner of the shares uint256 allowed = allowance(owner, msg.sender); - // TODO - RISK ask nick about , just compare allowance instead of approving. if (msg.sender != owner && allowed != type(uint256).max) { - // _approve(owner, msg.sender, allowed - shares); - require(assets <= allowed, "!allowance"); + require(shares <= allowed, "amount exceeds allowance"); + _approve(owner, msg.sender, allowed - shares); } - // _beforeWithdrawHook(assets, shares, owner); // Burn required shares from the sender FIRST _burn(owner, shares); // Optionally, transfer tokens from here to sender if (transferAssets) { - // underlying.safeTransfer(receiver, assets); - require(underlying.transfer(msg.sender, assets), "Must send tokens"); + require(underlying.transfer(receiver, assets), "Must send tokens"); } // If this withdrawal pushes the portion of stored collateral in the `connector` over a certain diff --git a/test/savings/savings-contract.spec.ts b/test/savings/savings-contract.spec.ts index 36d6f582..fd5f5025 100644 --- a/test/savings/savings-contract.spec.ts +++ b/test/savings/savings-contract.spec.ts @@ -32,6 +32,7 @@ import { } from "types/generated" import { getTimestamp } from "@utils/time" import { IModuleBehaviourContext, shouldBehaveLikeModule } from "../shared/Module.behaviour" +// import { IERC4626BehaviourContext, shouldBehaveLikeERC4626 } from "../shared/ERC4626.behaviour" interface Balances { totalCredits: BN @@ -72,6 +73,61 @@ interface ConfigRedeemAndUnwrap { output: MockERC20 // Asset to unwrap from underlying router: ExposedMasset | FeederPool | MockERC20 // Router address = mAsset || feederPool } + +enum ContractFns { + // deposits + DEPOSIT_SAVINGS, + DEPOSIT, + // mints + MINT, + // redeems + REDEEM_CREDITS, + REDEEM, + // withdraws + REDEEM_UNDERLYING, + WITHDRAW + +} +interface ContractFnType { + type: ContractFns + name: string + fn: string + fnReceiver?: string + fnReferrer?: string + event: string +} + +const depositSavingsFn: ContractFnType = { + type: ContractFns.DEPOSIT_SAVINGS, + name: "depositSavings", + fn: "depositSavings(uint256)", + fnReceiver: "depositSavings(uint256,address)", + fnReferrer: "depositSavings(uint256,address,address)", + event: "SavingsDeposited", +} + +const deposit4626Fn: ContractFnType = { + type: ContractFns.DEPOSIT, + name: "deposit 4626", + fn: "deposit(uint256,address)", + fnReceiver: "deposit(uint256,address)", + fnReferrer: "deposit(uint256,address,address)", + event: "Deposit", +} + +const redeemCreditsFn: ContractFnType = { + type: ContractFns.REDEEM_CREDITS, + name: "redeemCredits", + fn: "redeemCredits(uint256)", + event: "CreditsRedeemed", +} +const redeem4626Fn: ContractFnType = { + type: ContractFns.REDEEM, + name: "redeem 4626", + fn: "redeem(uint256,address,address)", + event: "Withdraw", +} + // MockERC20 & Masset const underlyingToCredits = (amount: BN | number, exchangeRate: BN): BN => BN.from(amount).mul(fullScale).div(exchangeRate).add(1) @@ -146,6 +202,7 @@ describe("SavingsContract", async () => { let bob: Account let charlie: Account const ctx: Partial = {} + // const ctxVault: Partial = {} const initialExchangeRate = simpleToExactAmount(1, 17) let mAssetMachine: MassetMachine @@ -199,6 +256,15 @@ describe("SavingsContract", async () => { }) shouldBehaveLikeModule(ctx as IModuleBehaviourContext) }) + // describe("behave like a Vault ERC4626", async () => { + // beforeEach(async () => { + // await createNewSavingsContract() + // ctxVault.vault = savingsContract + // ctxVault.token = masset + // ctx.sa = sa + // }) + // shouldBehaveLikeERC4626(ctx as IERC4626BehaviourContext) + // }) }) describe("constructor", async () => { @@ -348,143 +414,170 @@ describe("SavingsContract", async () => { }) }) - context("using depositSavings", async () => { - before(async () => { - await createNewSavingsContract() - }) - afterEach(async () => { - const data = await getData(savingsContract, alice) - expect(exchangeRateHolds(data), "Exchange rate must hold") - }) - it("should fail when amount is zero", async () => { - await expect(savingsContract["depositSavings(uint256)"](ZERO)).to.be.revertedWith("Must deposit something") - }) - it("should fail when beneficiary is 0", async () => { - await expect(savingsContract["depositSavings(uint256,address)"](1, ZERO_ADDRESS)).to.be.revertedWith( - "Invalid beneficiary address", - ) - }) - it("should fail if the user has no balance", async () => { - // Approve first - await masset.connect(sa.dummy1.signer).approve(savingsContract.address, simpleToExactAmount(1, 18)) - - // Deposit - await expect( - savingsContract.connect(sa.dummy1.signer)["depositSavings(uint256)"](simpleToExactAmount(1, 18)), - ).to.be.revertedWith("VM Exception") - }) - it("should deposit the mUSD and assign credits to the saver", async () => { - const dataBefore = await getData(savingsContract, sa.default) - const depositAmount = simpleToExactAmount(1, 18) - - // 1. Approve the savings contract to spend mUSD - await masset.approve(savingsContract.address, depositAmount) - // 2. Deposit the mUSD - const tx = savingsContract["depositSavings(uint256)"](depositAmount) - const expectedCredits = underlyingToCredits(depositAmount, initialExchangeRate) - await expect(tx).to.emit(savingsContract, "SavingsDeposited").withArgs(sa.default.address, depositAmount, expectedCredits) - - const dataAfter = await getData(savingsContract, sa.default) - expect(dataAfter.balances.userCredits).eq(expectedCredits, "Must receive some savings credits") - expect(dataAfter.balances.totalCredits).eq(expectedCredits) - expect(dataAfter.balances.user).eq(dataBefore.balances.user.sub(depositAmount)) - expect(dataAfter.balances.contract).eq(simpleToExactAmount(1, 18)) - }) - it("allows alice to deposit to beneficiary (bob.address)", async () => { - const dataBefore = await getData(savingsContract, bob) - const depositAmount = simpleToExactAmount(1, 18) - - await masset.approve(savingsContract.address, depositAmount) - - const tx = savingsContract.connect(alice.signer)["depositSavings(uint256,address)"](depositAmount, bob.address) - const expectedCredits = underlyingToCredits(depositAmount, initialExchangeRate) - await expect(tx).to.emit(savingsContract, "SavingsDeposited").withArgs(bob.address, depositAmount, expectedCredits) - const dataAfter = await getData(savingsContract, bob) - expect(dataAfter.balances.userCredits).eq(expectedCredits, "Must receive some savings credits") - expect(dataAfter.balances.totalCredits).eq(expectedCredits.mul(2)) - expect(dataAfter.balances.user).eq(dataBefore.balances.user) - expect(dataAfter.balances.contract).eq(dataBefore.balances.contract.add(simpleToExactAmount(1, 18))) - }) - it("allows alice to deposit to beneficiary (bob.address) with a referral (charlie.address)", async () => { - const dataBefore = await getData(savingsContract, bob) - const depositAmount = simpleToExactAmount(1, 18) - - await masset.approve(savingsContract.address, depositAmount) - - const tx = savingsContract - .connect(alice.signer) - ["depositSavings(uint256,address,address)"](depositAmount, bob.address, charlie.address) - await expect(tx).to.emit(savingsContract, "Referral").withArgs(charlie.address, bob.address, depositAmount) - - const expectedCredits = underlyingToCredits(depositAmount, initialExchangeRate) - await expect(tx).to.emit(savingsContract, "SavingsDeposited").withArgs(bob.address, depositAmount, expectedCredits) - const dataAfter = await getData(savingsContract, bob) - expect(dataAfter.balances.userCredits, "Must receive some savings credits").eq( - dataBefore.balances.userCredits.add(expectedCredits), - ) - expect(dataAfter.balances.totalCredits, "Total credits").eq(dataBefore.balances.totalCredits.add(expectedCredits)) - expect(dataAfter.balances.user).eq(dataBefore.balances.user) - expect(dataAfter.balances.contract, "Contract balance").eq(dataBefore.balances.contract.add(simpleToExactAmount(1, 18))) - }) - context("when there is some interest to collect from the manager", async () => { - const deposit = simpleToExactAmount(10, 18) - const interest = simpleToExactAmount(10, 18) + async function validateDeposits(depositFN: ContractFnType) { + const depositToSender = + (contract: SavingsContract) => + (...args) => { + if (depositFN.type === ContractFns.DEPOSIT_SAVINGS) { + return contract[depositFN.fn](...args) + } + return contract[depositFN.fn](...args, contract.signer.getAddress()) + } + async function expectToEmitDepositEvent(tx, sender: string, receiver: string, depositAmount: BN, expectedCredits: BN) { + switch (depositFN.type) { + case ContractFns.DEPOSIT: + await expect(tx) + .to.emit(savingsContract, depositFN.event) + .withArgs(sender, receiver, depositAmount, expectedCredits) + break + case ContractFns.DEPOSIT_SAVINGS: + default: + await expect(tx).to.emit(savingsContract, depositFN.event).withArgs(receiver, depositAmount, expectedCredits) + break + } + } + context(`using ${depositFN.name}`, async () => { before(async () => { await createNewSavingsContract() - await masset.approve(savingsContract.address, deposit) }) afterEach(async () => { const data = await getData(savingsContract, alice) expect(exchangeRateHolds(data), "Exchange rate must hold") }) - it("should collect the interest and update the exchange rate before issuance", async () => { - // Get the total balances - const stateBefore = await getData(savingsContract, alice) - expect(stateBefore.exchangeRate).to.equal(initialExchangeRate) - - // Deposit first to get some savings in the basket - await savingsContract["depositSavings(uint256)"](deposit) - - const stateMiddle = await getData(savingsContract, alice) - expect(stateMiddle.exchangeRate).to.equal(initialExchangeRate) - expect(stateMiddle.balances.contract).to.equal(deposit) - expect(stateMiddle.balances.totalCredits).to.equal(underlyingToCredits(deposit, initialExchangeRate)) - - // Set up the mAsset with some interest - await masset.setAmountForCollectInterest(interest) - - await ethers.provider.send("evm_increaseTime", [ONE_DAY.toNumber()]) - await ethers.provider.send("evm_mine", []) - - // Bob deposits into the contract - await masset.transfer(bob.address, deposit) - await masset.connect(bob.signer).approve(savingsContract.address, deposit) - const tx = savingsContract.connect(bob.signer)["depositSavings(uint256)"](deposit) - // Bob collects interest, to the benefit of Alice - // Expected rate = 1e17 + 1e17-1 - const expectedExchangeRate = simpleToExactAmount(2, 17) - await expect(tx).to.emit(savingsContract, "ExchangeRateUpdated").withArgs(expectedExchangeRate, interest) - - // Alice gets the benefit of the new exchange rate - const stateEnd = await getData(savingsContract, alice) - expect(stateEnd.exchangeRate).eq(expectedExchangeRate) - expect(stateEnd.balances.contract).eq(deposit.mul(3)) - const aliceBalance = await savingsContract.balanceOfUnderlying(alice.address) - expect(simpleToExactAmount(20, 18)).eq(aliceBalance) - - // Bob gets credits at the NEW exchange rate - const bobData = await getData(savingsContract, bob) - expect(bobData.balances.userCredits).eq(underlyingToCredits(deposit, stateEnd.exchangeRate)) - expect(stateEnd.balances.totalCredits).eq(bobData.balances.userCredits.add(stateEnd.balances.userCredits)) - const bobBalance = await savingsContract.balanceOfUnderlying(bob.address) - expect(bobBalance).eq(deposit) - expect(bobBalance.add(aliceBalance)).eq(deposit.mul(3), "Individual balances cannot exceed total") - - expect(exchangeRateHolds(stateEnd), "Exchange rate must hold") + it("should fail when amount is zero", async () => { + await expect(depositToSender(savingsContract)(ZERO)).to.be.revertedWith("Must deposit something") + }) + it("should fail when beneficiary is 0", async () => { + await expect(savingsContract[depositFN.fnReceiver](1, ZERO_ADDRESS)).to.be.revertedWith("Invalid beneficiary address") + }) + it("should fail if the user has no balance", async () => { + // Approve first + await masset.connect(sa.dummy1.signer).approve(savingsContract.address, simpleToExactAmount(1, 18)) + + // Deposit + await expect(depositToSender(savingsContract.connect(sa.dummy1.signer))(simpleToExactAmount(1, 18))).to.be.revertedWith( + "VM Exception", + ) + }) + it("should deposit the mUSD and assign credits to the saver", async () => { + const dataBefore = await getData(savingsContract, sa.default) + const depositAmount = simpleToExactAmount(1, 18) + + // 1. Approve the savings contract to spend mUSD + await masset.approve(savingsContract.address, depositAmount) + // 2. Deposit the mUSD + const tx = depositToSender(savingsContract)(depositAmount) + const expectedCredits = underlyingToCredits(depositAmount, initialExchangeRate) + + await expectToEmitDepositEvent(tx, sa.default.address, sa.default.address, depositAmount, expectedCredits) + const dataAfter = await getData(savingsContract, sa.default) + expect(dataAfter.balances.userCredits).eq(expectedCredits, "Must receive some savings credits") + expect(dataAfter.balances.totalCredits).eq(expectedCredits) + expect(dataAfter.balances.user).eq(dataBefore.balances.user.sub(depositAmount)) + expect(dataAfter.balances.contract).eq(simpleToExactAmount(1, 18)) + }) + it("allows alice to deposit to beneficiary (bob.address)", async () => { + const dataBefore = await getData(savingsContract, bob) + const depositAmount = simpleToExactAmount(1, 18) + + await masset.approve(savingsContract.address, depositAmount) + + const tx = savingsContract.connect(alice.signer)[depositFN.fnReceiver](depositAmount, bob.address) + const expectedCredits = underlyingToCredits(depositAmount, initialExchangeRate) + await expectToEmitDepositEvent(tx, alice.address, bob.address, depositAmount, expectedCredits) + const dataAfter = await getData(savingsContract, bob) + expect(dataAfter.balances.userCredits).eq(expectedCredits, "Must receive some savings credits") + expect(dataAfter.balances.totalCredits).eq(expectedCredits.mul(2)) + expect(dataAfter.balances.user).eq(dataBefore.balances.user) + expect(dataAfter.balances.contract).eq(dataBefore.balances.contract.add(simpleToExactAmount(1, 18))) + }) + it("allows alice to deposit to beneficiary (bob.address) with a referral (charlie.address)", async () => { + const dataBefore = await getData(savingsContract, bob) + const depositAmount = simpleToExactAmount(1, 18) + + await masset.approve(savingsContract.address, depositAmount) + + const tx = savingsContract.connect(alice.signer)[depositFN.fnReferrer](depositAmount, bob.address, charlie.address) + await expect(tx).to.emit(savingsContract, "Referral").withArgs(charlie.address, bob.address, depositAmount) + + const expectedCredits = underlyingToCredits(depositAmount, initialExchangeRate) + await expectToEmitDepositEvent(tx, alice.address, bob.address, depositAmount, expectedCredits) + const dataAfter = await getData(savingsContract, bob) + expect(dataAfter.balances.userCredits, "Must receive some savings credits").eq( + dataBefore.balances.userCredits.add(expectedCredits), + ) + expect(dataAfter.balances.totalCredits, "Total credits").eq(dataBefore.balances.totalCredits.add(expectedCredits)) + expect(dataAfter.balances.user).eq(dataBefore.balances.user) + expect(dataAfter.balances.contract, "Contract balance").eq(dataBefore.balances.contract.add(simpleToExactAmount(1, 18))) + }) + context("when there is some interest to collect from the manager", async () => { + const deposit = simpleToExactAmount(10, 18) + const interest = simpleToExactAmount(10, 18) + before(async () => { + await createNewSavingsContract() + await masset.approve(savingsContract.address, deposit) + }) + afterEach(async () => { + const data = await getData(savingsContract, alice) + expect(exchangeRateHolds(data), "Exchange rate must hold") + }) + it("should collect the interest and update the exchange rate before issuance", async () => { + // Get the total balances + const stateBefore = await getData(savingsContract, alice) + expect(stateBefore.exchangeRate).to.equal(initialExchangeRate) + + // Deposit first to get some savings in the basket + await depositToSender(savingsContract)(deposit) + + const stateMiddle = await getData(savingsContract, alice) + expect(stateMiddle.exchangeRate).to.equal(initialExchangeRate) + expect(stateMiddle.balances.contract).to.equal(deposit) + expect(stateMiddle.balances.totalCredits).to.equal(underlyingToCredits(deposit, initialExchangeRate)) + + // Set up the mAsset with some interest + await masset.setAmountForCollectInterest(interest) + + await ethers.provider.send("evm_increaseTime", [ONE_DAY.toNumber()]) + await ethers.provider.send("evm_mine", []) + + // Bob deposits into the contract + await masset.transfer(bob.address, deposit) + await masset.connect(bob.signer).approve(savingsContract.address, deposit) + const tx = depositToSender(savingsContract.connect(bob.signer))(deposit) + // Bob collects interest, to the benefit of Alice + // Expected rate = 1e17 + 1e17-1 + const expectedExchangeRate = simpleToExactAmount(2, 17) + await expect(tx).to.emit(savingsContract, "ExchangeRateUpdated").withArgs(expectedExchangeRate, interest) + + // Alice gets the benefit of the new exchange rate + const stateEnd = await getData(savingsContract, alice) + expect(stateEnd.exchangeRate).eq(expectedExchangeRate) + expect(stateEnd.balances.contract).eq(deposit.mul(3)) + const aliceBalance = await savingsContract.balanceOfUnderlying(alice.address) + expect(simpleToExactAmount(20, 18)).eq(aliceBalance) + + // Bob gets credits at the NEW exchange rate + const bobData = await getData(savingsContract, bob) + expect(bobData.balances.userCredits).eq(underlyingToCredits(deposit, stateEnd.exchangeRate)) + expect(stateEnd.balances.totalCredits).eq(bobData.balances.userCredits.add(stateEnd.balances.userCredits)) + const bobBalance = await savingsContract.balanceOfUnderlying(bob.address) + expect(bobBalance).eq(deposit) + expect(bobBalance.add(aliceBalance)).eq(deposit.mul(3), "Individual balances cannot exceed total") + + expect(exchangeRateHolds(stateEnd), "Exchange rate must hold") + }) }) }) + } + + context("deposits", async () => { + // V1,V2,V3 + await validateDeposits(depositSavingsFn) + // ERC4626 + await validateDeposits(deposit4626Fn) }) }) + describe("checking the view methods", () => { const aliceCredits = simpleToExactAmount(100, 18).add(1) const aliceUnderlying = simpleToExactAmount(20, 18) @@ -554,84 +647,202 @@ describe("SavingsContract", async () => { expect(await savingsContract.creditsToUnderlying(uToC4)).eq(BN.from(12986123876)) }) }) - describe("redeeming", async () => { + async function validateRedeems(contractFn:ContractFnType){ + const redeemToSender = + (contract: SavingsContract) => + (...args) => { + if (contractFn.type === ContractFns.REDEEM_CREDITS) { + return contract[contractFn.fn](...args) + } + return contract[contractFn.fn](...args, contract.signer.getAddress(),contract.signer.getAddress()) + } + async function expectToEmitRedeemEvent(tx, sender: string, receiver: string, assets: BN, shares: BN) { + switch (contractFn.type) { + case ContractFns.REDEEM: + await expect(tx) + .to.emit(savingsContract, contractFn.event) + .withArgs(sender, receiver,receiver, assets, shares) + break + case ContractFns.REDEEM_CREDITS: + default: + await expect(tx).to.emit(savingsContract, contractFn.event).withArgs(sender, shares, assets) + break + } + } + context(`scenario ${contractFn.name}`, async () => { + before(async () => { + await createNewSavingsContract() + }) + it("should fail when input is zero", async () => { + await expect(redeemToSender(savingsContract)(ZERO)).to.be.revertedWith("Must withdraw something") + }) + it("should fail when user doesn't have credits", async () => { + const amt = BN.from(10) + await expect(redeemToSender(savingsContract.connect(sa.other.signer))(amt)).to.be.revertedWith("VM Exception") + }) + context(`using ${contractFn.name}`, async () => { + const deposit = simpleToExactAmount(10, 18) + const credits = underlyingToCredits(deposit, initialExchangeRate) + const interest = simpleToExactAmount(10, 18) + beforeEach(async () => { + await createNewSavingsContract() + await masset.approve(savingsContract.address, simpleToExactAmount(1, 21)) + await savingsContract.preDeposit(deposit, alice.address) + }) + afterEach(async () => { + const data = await getData(savingsContract, alice) + expect(exchangeRateHolds(data), "Exchange rate must hold") + }) + // test the balance calcs here.. credit to masset, and public calcs + it("should redeem a specific amount of credits", async () => { + // calculates underlying/credits + const creditsToWithdraw = simpleToExactAmount(5, 18) + const expectedWithdrawal = creditsToUnderlying(creditsToWithdraw, initialExchangeRate) + const dataBefore = await getData(savingsContract, alice) + const tx = redeemToSender(savingsContract)(creditsToWithdraw) + await expectToEmitRedeemEvent(tx, alice.address, alice.address, expectedWithdrawal, creditsToWithdraw) + // await tx.wait() + const dataAfter = await getData(savingsContract, alice) + // burns credits from sender + expect(dataAfter.balances.userCredits).eq(dataBefore.balances.userCredits.sub(creditsToWithdraw)) + expect(dataAfter.balances.totalCredits).eq(dataBefore.balances.totalCredits.sub(creditsToWithdraw)) + // transfers tokens to sender + expect(dataAfter.balances.user).eq(dataBefore.balances.user.add(expectedWithdrawal)) + expect(dataAfter.balances.contract).eq(dataBefore.balances.contract.sub(expectedWithdrawal)) + }) + it("should redeem a specific amount of credits and collect interest", async () => { + // calculates underlying/credits automateInterestCollection + const creditsToWithdraw = simpleToExactAmount(5, 18) + const expectedWithdrawal = creditsToUnderlying(creditsToWithdraw, initialExchangeRate) + const dataBefore = await getData(savingsContract, alice) + // Enable automateInterestCollection + await savingsContract.connect(sa.governor.signer).automateInterestCollectionFlag(true) + + const tx = redeemToSender(savingsContract)(creditsToWithdraw) + await expectToEmitRedeemEvent(tx, alice.address, alice.address, expectedWithdrawal, creditsToWithdraw) + // await tx.wait() + const dataAfter = await getData(savingsContract, alice) + // burns credits from sender + expect(dataAfter.balances.userCredits).eq(dataBefore.balances.userCredits.sub(creditsToWithdraw)) + expect(dataAfter.balances.totalCredits).eq(dataBefore.balances.totalCredits.sub(creditsToWithdraw)) + // transfers tokens to sender + expect(dataAfter.balances.user).eq(dataBefore.balances.user.add(expectedWithdrawal)) + expect(dataAfter.balances.contract).eq(dataBefore.balances.contract.sub(expectedWithdrawal)) + }) + it("collects interest and credits to saver before redemption", async () => { + const expectedExchangeRate = simpleToExactAmount(2, 17) + await masset.setAmountForCollectInterest(interest) + const dataBefore = await getData(savingsContract, alice) + await redeemToSender(savingsContract)(credits) + const dataAfter = await getData(savingsContract, alice) + expect(dataAfter.balances.totalCredits).eq(BN.from(0)) + // User receives their deposit back + interest + assertBNClose(dataAfter.balances.user, dataBefore.balances.user.add(deposit).add(interest), 100) + // Exchange rate updates + expect(dataAfter.exchangeRate).eq(expectedExchangeRate) + }) + }) + context("with a connector that surpasses limit", async () => { + const deposit = simpleToExactAmount(100, 18) + const redemption = underlyingToCredits(simpleToExactAmount(51, 18), initialExchangeRate) + before(async () => { + await createNewSavingsContract() + const connector = await ( + await new MockConnector__factory(sa.default.signer) + ).deploy(savingsContract.address, masset.address) + + await masset.approve(savingsContract.address, simpleToExactAmount(1, 21)) + await savingsContract.preDeposit(deposit, alice.address) + + await savingsContract.connect(sa.governor.signer).setConnector(connector.address) + + await ethers.provider.send("evm_increaseTime", [ONE_HOUR.mul(4).add(1).toNumber()]) + await ethers.provider.send("evm_mine", []) + + const data = await getData(savingsContract, alice) + expect(data.connector.balance).eq(deposit.mul(data.connector.fraction).div(fullScale)) + expect(data.balances.contract).eq(deposit.sub(data.connector.balance)) + expect(data.exchangeRate).eq(initialExchangeRate) + }) + afterEach(async () => { + const data = await getData(savingsContract, alice) + expect(exchangeRateHolds(data), "Exchange rate must hold") + }) + it("triggers poke and deposits to connector if the threshold is hit", async () => { + // in order to reach 40%, must redeem > 51 + const dataBefore = await getData(savingsContract, alice) + const poke = await getExpectedPoke(dataBefore, redemption) + + const tx = redeemToSender(savingsContract)(redemption) + await expectToEmitRedeemEvent(tx, alice.address, alice.address, simpleToExactAmount(51, 18), redemption) + // Remaining balance is 49, with 20 in the connector + await expect(tx).to.emit(savingsContract, "Poked").withArgs(dataBefore.connector.balance, poke.ideal, BN.from(0)) + + const dataAfter = await getData(savingsContract, alice) + expect(dataAfter.balances.contract).eq(simpleToExactAmount("39.2", 18)) + }) + it("errors if triggered again within 4h", async () => {}) + }) + }) + } + context("using redeem(uint256) (deprecated)", async () => { + beforeEach(async () => { + await createNewSavingsContract() + await masset.approve(savingsContract.address, simpleToExactAmount(10, 18)) + await savingsContract["depositSavings(uint256)"](simpleToExactAmount(1, 18)) + }) + it("should fail when input is zero", async () => { + await expect(savingsContract["redeem(uint256)"](ZERO)).to.be.revertedWith("Must withdraw something") + }) + it("should fail when user doesn't have credits", async () => { + const amt = BN.from(10) + await expect(savingsContract.connect(sa.other.signer)["redeem(uint256)"](amt)).to.be.revertedWith("VM Exception") + }) + it("should redeem when user has balance", async () => { + const redemptionAmount = simpleToExactAmount(5, 18) + const balancesBefore = await getData(savingsContract, sa.default) + + const tx = savingsContract["redeem(uint256)"](redemptionAmount) + const exchangeRate = initialExchangeRate + const underlying = creditsToUnderlying(redemptionAmount, exchangeRate) + await expect(tx).to.emit(savingsContract, "CreditsRedeemed").withArgs(sa.default.address, redemptionAmount, underlying) + const dataAfter = await getData(savingsContract, sa.default) + expect(balancesBefore.balances.contract.sub(underlying)).to.equal(dataAfter.balances.contract) + + expect(balancesBefore.balances.user.add(underlying)).to.equal(dataAfter.balances.user) + }) + it("should withdraw the mUSD and burn the credits", async () => { + const redemptionAmount = simpleToExactAmount(1, 18) + const creditsBefore = await savingsContract.creditBalances(sa.default.address) + const mUSDBefore = await masset.balanceOf(sa.default.address) + // Redeem all the credits + await savingsContract["redeem(uint256)"](creditsBefore) + + const creditsAfter = await savingsContract.creditBalances(sa.default.address) + const mUSDAfter = await masset.balanceOf(sa.default.address) + expect(creditsAfter, "Must burn all the credits").eq(BN.from(0)) + expect(mUSDAfter, "Must receive back mUSD").eq(mUSDBefore.add(redemptionAmount)) + }) + }) + describe("redeems", async () => { + // V1,V2,V3 + await validateRedeems(redeemCreditsFn); + // ERC4626 + await validateRedeems(redeem4626Fn); + }) + }) + describe("withdrawing", async () => { before(async () => { await createNewSavingsContract() }) it("should fail when input is zero", async () => { - await expect(savingsContract["redeem(uint256)"](ZERO)).to.be.revertedWith("Must withdraw something") - await expect(savingsContract.redeemCredits(ZERO)).to.be.revertedWith("Must withdraw something") await expect(savingsContract.redeemUnderlying(ZERO)).to.be.revertedWith("Must withdraw something") }) it("should fail when user doesn't have credits", async () => { const amt = BN.from(10) - await expect(savingsContract.connect(sa.other.signer)["redeem(uint256)"](amt)).to.be.revertedWith("VM Exception") - await expect(savingsContract.connect(sa.other.signer).redeemCredits(amt)).to.be.revertedWith("VM Exception") await expect(savingsContract.connect(sa.other.signer).redeemUnderlying(amt)).to.be.revertedWith("VM Exception") }) - context("using redeemCredits", async () => { - const deposit = simpleToExactAmount(10, 18) - const credits = underlyingToCredits(deposit, initialExchangeRate) - const interest = simpleToExactAmount(10, 18) - beforeEach(async () => { - await createNewSavingsContract() - await masset.approve(savingsContract.address, simpleToExactAmount(1, 21)) - await savingsContract.preDeposit(deposit, alice.address) - }) - afterEach(async () => { - const data = await getData(savingsContract, alice) - expect(exchangeRateHolds(data), "Exchange rate must hold") - }) - // test the balance calcs here.. credit to masset, and public calcs - it("should redeem a specific amount of credits", async () => { - // calculates underlying/credits - const creditsToWithdraw = simpleToExactAmount(5, 18) - const expectedWithdrawal = creditsToUnderlying(creditsToWithdraw, initialExchangeRate) - const dataBefore = await getData(savingsContract, alice) - const tx = savingsContract.redeemCredits(creditsToWithdraw) - await expect(tx).to.emit(savingsContract, "CreditsRedeemed").withArgs(alice.address, creditsToWithdraw, expectedWithdrawal) - // await tx.wait() - const dataAfter = await getData(savingsContract, alice) - // burns credits from sender - expect(dataAfter.balances.userCredits).eq(dataBefore.balances.userCredits.sub(creditsToWithdraw)) - expect(dataAfter.balances.totalCredits).eq(dataBefore.balances.totalCredits.sub(creditsToWithdraw)) - // transfers tokens to sender - expect(dataAfter.balances.user).eq(dataBefore.balances.user.add(expectedWithdrawal)) - expect(dataAfter.balances.contract).eq(dataBefore.balances.contract.sub(expectedWithdrawal)) - }) - it("should redeem a specific amount of credits and collect interest", async () => { - // calculates underlying/credits automateInterestCollection - const creditsToWithdraw = simpleToExactAmount(5, 18) - const expectedWithdrawal = creditsToUnderlying(creditsToWithdraw, initialExchangeRate) - const dataBefore = await getData(savingsContract, alice) - // Enable automateInterestCollection - await savingsContract.connect(sa.governor.signer).automateInterestCollectionFlag(true) - - const tx = savingsContract.redeemCredits(creditsToWithdraw) - await expect(tx).to.emit(savingsContract, "CreditsRedeemed").withArgs(alice.address, creditsToWithdraw, expectedWithdrawal) - // await tx.wait() - const dataAfter = await getData(savingsContract, alice) - // burns credits from sender - expect(dataAfter.balances.userCredits).eq(dataBefore.balances.userCredits.sub(creditsToWithdraw)) - expect(dataAfter.balances.totalCredits).eq(dataBefore.balances.totalCredits.sub(creditsToWithdraw)) - // transfers tokens to sender - expect(dataAfter.balances.user).eq(dataBefore.balances.user.add(expectedWithdrawal)) - expect(dataAfter.balances.contract).eq(dataBefore.balances.contract.sub(expectedWithdrawal)) - }) - it("collects interest and credits to saver before redemption", async () => { - const expectedExchangeRate = simpleToExactAmount(2, 17) - await masset.setAmountForCollectInterest(interest) - const dataBefore = await getData(savingsContract, alice) - await savingsContract.redeemCredits(credits) - const dataAfter = await getData(savingsContract, alice) - expect(dataAfter.balances.totalCredits).eq(BN.from(0)) - // User receives their deposit back + interest - assertBNClose(dataAfter.balances.user, dataBefore.balances.user.add(deposit).add(interest), 100) - // Exchange rate updates - expect(dataAfter.exchangeRate).eq(expectedExchangeRate) - }) - }) context("using redeemUnderlying", async () => { const deposit = simpleToExactAmount(10, 18) const interest = simpleToExactAmount(10, 18) @@ -691,82 +902,6 @@ describe("SavingsContract", async () => { expect(dataAfter.exchangeRate).eq(dataBefore.exchangeRate) }) }) - context("with a connector that surpasses limit", async () => { - const deposit = simpleToExactAmount(100, 18) - const redemption = underlyingToCredits(simpleToExactAmount(51, 18), initialExchangeRate) - before(async () => { - await createNewSavingsContract() - const connector = await ( - await new MockConnector__factory(sa.default.signer) - ).deploy(savingsContract.address, masset.address) - - await masset.approve(savingsContract.address, simpleToExactAmount(1, 21)) - await savingsContract.preDeposit(deposit, alice.address) - - await savingsContract.connect(sa.governor.signer).setConnector(connector.address) - - await ethers.provider.send("evm_increaseTime", [ONE_HOUR.mul(4).add(1).toNumber()]) - await ethers.provider.send("evm_mine", []) - - const data = await getData(savingsContract, alice) - expect(data.connector.balance).eq(deposit.mul(data.connector.fraction).div(fullScale)) - expect(data.balances.contract).eq(deposit.sub(data.connector.balance)) - expect(data.exchangeRate).eq(initialExchangeRate) - }) - afterEach(async () => { - const data = await getData(savingsContract, alice) - expect(exchangeRateHolds(data), "Exchange rate must hold") - }) - it("triggers poke and deposits to connector if the threshold is hit", async () => { - // in order to reach 40%, must redeem > 51 - const dataBefore = await getData(savingsContract, alice) - const poke = await getExpectedPoke(dataBefore, redemption) - - const tx = savingsContract.redeemCredits(redemption) - await expect(tx) - .to.emit(savingsContract, "CreditsRedeemed") - .withArgs(alice.address, redemption, simpleToExactAmount(51, 18)) - // Remaining balance is 49, with 20 in the connector - await expect(tx).to.emit(savingsContract, "Poked").withArgs(dataBefore.connector.balance, poke.ideal, BN.from(0)) - - const dataAfter = await getData(savingsContract, alice) - expect(dataAfter.balances.contract).eq(simpleToExactAmount("39.2", 18)) - }) - it("errors if triggered again within 4h", async () => {}) - }) - - context("using redeem (depcrecated)", async () => { - beforeEach(async () => { - await createNewSavingsContract() - await masset.approve(savingsContract.address, simpleToExactAmount(10, 18)) - await savingsContract["depositSavings(uint256)"](simpleToExactAmount(1, 18)) - }) - it("should redeem when user has balance", async () => { - const redemptionAmount = simpleToExactAmount(5, 18) - const balancesBefore = await getData(savingsContract, sa.default) - - const tx = savingsContract["redeem(uint256)"](redemptionAmount) - const exchangeRate = initialExchangeRate - const underlying = creditsToUnderlying(redemptionAmount, exchangeRate) - await expect(tx).to.emit(savingsContract, "CreditsRedeemed").withArgs(sa.default.address, redemptionAmount, underlying) - const dataAfter = await getData(savingsContract, sa.default) - expect(balancesBefore.balances.contract.sub(underlying)).to.equal(dataAfter.balances.contract) - - expect(balancesBefore.balances.user.add(underlying)).to.equal(dataAfter.balances.user) - }) - it("should withdraw the mUSD and burn the credits", async () => { - const redemptionAmount = simpleToExactAmount(1, 18) - const creditsBefore = await savingsContract.creditBalances(sa.default.address) - const mUSDBefore = await masset.balanceOf(sa.default.address) - // Redeem all the credits - await savingsContract["redeem(uint256)"](creditsBefore) - - const creditsAfter = await savingsContract.creditBalances(sa.default.address) - const mUSDAfter = await masset.balanceOf(sa.default.address) - expect(creditsAfter, "Must burn all the credits").eq(BN.from(0)) - expect(mUSDAfter, "Must receive back mUSD").eq(mUSDBefore.add(redemptionAmount)) - }) - }) }) describe("setting poker", () => { diff --git a/test/shared/ERC4626.behaviours.ts b/test/shared/ERC4626.behaviours.ts new file mode 100644 index 00000000..c5b1bd1d --- /dev/null +++ b/test/shared/ERC4626.behaviours.ts @@ -0,0 +1,173 @@ + +import hre, { ethers } from "hardhat"; +import { expect } from "chai" +import { Signer } from "ethers" +import { simpleToExactAmount, BN } from "@utils/math" +import { IERC4626Vault, IERC4626Vault__factory, AbstractVault, AbstractVault__factory, MockNexus, ERC20 } from "types/generated" +import { Account } from "types" +import { MassetMachine, MassetDetails, StandardAccounts } from "@utils/machines" +import { ZERO_ADDRESS } from "@utils/constants" + + + +export interface IERC4626BehaviourContext { + vault: IERC4626Vault + asset: ERC20 + sa: StandardAccounts + mAssetMachine: MassetMachine + owner: Account + receiver: Account + anotherAccount: Account + details: MassetDetails +} + +export async function shouldBehaveLikeERC4626(ctx: IERC4626BehaviourContext, errorPrefix: string, initialSupply: BN): Promise { + + const assetsAmount = simpleToExactAmount(1, await ctx.asset.decimals()) + const sharesAmount = simpleToExactAmount(10, await ctx.asset.decimals()) + describe('ERC4626', () => { + + before("init contract", async () => { + }) + beforeEach(async () => { /* before each context */ }) + + describe("constructor", async () => { + it("should properly store valid arguments", async () => { + expect(await ctx.vault.asset(), "asset").to.eq(ctx.asset.address); + }) + }) + + // + describe("deposit", async () => { + beforeEach(async () => { /* before each context */ }) + + it('deposit should ...', async () => { + await ctx.asset.approve(ctx.vault.address, simpleToExactAmount(1, 21)) + + const tx = await ctx.vault.connect(ctx.owner.signer).deposit(assetsAmount, ctx.receiver.address) + // Verify events, storage change, balance, etc. + // await expect(tx).to.emit(abstractVault, "EVENT-NAME").withArgs("ARGUMENT 1", "ARGUMENT 2"); + + }); + it('fails if ...', async () => { + await expect(ctx.vault.connect(ctx.owner.signer).deposit(assetsAmount, ctx.receiver.address), "fails due to ").to.be.revertedWith("EXPECTED ERROR"); + }); + }); + + + // + describe("mint", async () => { + beforeEach(async () => { /* before each context */ }) + + it('mint should ...', async () => { + const tx = await ctx.vault.connect(ctx.owner.signer).mint(sharesAmount, ctx.receiver.address) + // Verify events, storage change, balance, etc. + // await expect(tx).to.emit(abstractVault, "EVENT-NAME").withArgs("ARGUMENT 1", "ARGUMENT 2"); + + }); + it('fails if ...', async () => { + await expect(ctx.vault.connect(ctx.owner.signer).mint(sharesAmount, ctx.receiver.address), "fails due to ").to.be.revertedWith("EXPECTED ERROR"); + }); + }); + + + // + describe("withdraw", async () => { + beforeEach(async () => { /* before each context */ }) + + it('withdraw should ...', async () => { + const tx = await ctx.vault.connect(ctx.owner.signer).withdraw(assetsAmount, ctx.receiver.address, ctx.owner.address) + // Verify events, storage change, balance, etc. + // await expect(tx).to.emit(abstractVault, "EVENT-NAME").withArgs("ARGUMENT 1", "ARGUMENT 2"); + + }); + it('fails if ...', async () => { + await expect(ctx.vault.connect(ctx.owner.signer).withdraw(assetsAmount, ctx.receiver.address, ctx.owner.address), "fails due to ").to.be.revertedWith("EXPECTED ERROR"); + }); + }); + + + // + describe("redeem", async () => { + beforeEach(async () => { /* before each context */ }) + + it('redeem should ...', async () => { + const tx = await ctx.vault.connect(ctx.owner.signer).redeem(sharesAmount, ctx.receiver.address, ctx.owner.address) + // Verify events, storage change, balance, etc. + // await expect(tx).to.emit(abstractVault, "EVENT-NAME").withArgs("ARGUMENT 1", "ARGUMENT 2"); + + }); + it('fails if ...', async () => { + await expect(ctx.vault.connect(ctx.owner.signer).redeem(sharesAmount, ctx.receiver.address, ctx.owner.address), "fails due to ").to.be.revertedWith("EXPECTED ERROR"); + }); + }); + + + describe("read only functions", async () => { + beforeEach(async () => { /* before each context */ }) + + it('previewDeposit should ...', async () => { + const response = await ctx.vault.previewDeposit(assetsAmount); + expect(response, "previewDeposit").to.eq("expected value"); + }); + + it('maxDeposit should ...', async () => { + const response = await ctx.vault.maxDeposit(ctx.owner.address); + expect(response, "maxDeposit").to.eq("expected value"); + }); + + it('previewMint should ...', async () => { + const response = await ctx.vault.previewMint(sharesAmount); + expect(response, "previewMint").to.eq("expected value"); + }); + + it('maxMint should ...', async () => { + const response = await ctx.vault.maxMint(ctx.owner.address); + expect(response, "maxMint").to.eq("expected value"); + }); + + + it('previewWithdraw should ...', async () => { + const response = await ctx.vault.previewWithdraw(assetsAmount); + expect(response, "previewWithdraw").to.eq("expected value"); + }); + + + it('maxWithdraw should ...', async () => { + const response = await ctx.vault.maxWithdraw(ctx.owner.address); + expect(response, "maxWithdraw").to.eq("expected value"); + }); + + it('previewRedeem should ...', async () => { + const response = await ctx.vault.previewRedeem(sharesAmount); + expect(response, "previewRedeem").to.eq("expected value"); + }); + + it('maxRedeem should ...', async () => { + const response = await ctx.vault.maxRedeem(ctx.owner.address); + expect(response, "maxRedeem").to.eq("expected value"); + }); + + it('totalAssets should ...', async () => { + const response = await ctx.vault.totalAssets(); + expect(response, "totalAssets").to.eq("expected value"); + }); + + + it('convertToAssets should ...', async () => { + const response = await ctx.vault.convertToAssets(sharesAmount); + expect(response, "convertToAssets").to.eq("expected value"); + }); + + + it('convertToShares should ...', async () => { + const response = await ctx.vault.convertToShares(assetsAmount); + expect(response, "convertToShares").to.eq("expected value"); + }); + + }); + + }); +} + +export default shouldBehaveLikeERC4626; From fe7b2f5163e0faeb6f6e6bd89766e627a3e852dc Mon Sep 17 00:00:00 2001 From: doncesarts Date: Fri, 8 Apr 2022 16:50:30 +0100 Subject: [PATCH 06/14] test: adds mint tests to SavingsContract --- test/savings/savings-contract.spec.ts | 595 ++++++++++++++++---------- 1 file changed, 364 insertions(+), 231 deletions(-) diff --git a/test/savings/savings-contract.spec.ts b/test/savings/savings-contract.spec.ts index fd5f5025..581de8d3 100644 --- a/test/savings/savings-contract.spec.ts +++ b/test/savings/savings-contract.spec.ts @@ -75,7 +75,7 @@ interface ConfigRedeemAndUnwrap { } enum ContractFns { - // deposits + // deposits DEPOSIT_SAVINGS, DEPOSIT, // mints @@ -85,8 +85,7 @@ enum ContractFns { REDEEM, // withdraws REDEEM_UNDERLYING, - WITHDRAW - + WITHDRAW, } interface ContractFnType { type: ContractFns @@ -114,7 +113,13 @@ const deposit4626Fn: ContractFnType = { fnReferrer: "deposit(uint256,address,address)", event: "Deposit", } - +const mint4626Fn: ContractFnType = { + type: ContractFns.MINT, + name: "mint 4626", + fn: "mint(uint256,address)", + fnReceiver: "mint(uint256,address)", + event: "Deposit", +} const redeemCreditsFn: ContractFnType = { type: ContractFns.REDEEM_CREDITS, name: "redeemCredits", @@ -127,6 +132,18 @@ const redeem4626Fn: ContractFnType = { fn: "redeem(uint256,address,address)", event: "Withdraw", } +const redeemUnderlyingFn: ContractFnType = { + type: ContractFns.REDEEM_UNDERLYING, + name: "redeemUnderlying", + fn: "redeemUnderlying(uint256)", + event: "CreditsRedeemed", +} +const withdraw4626Fn: ContractFnType = { + type: ContractFns.WITHDRAW, + name: "withdraw 4626", + fn: "withdraw(uint256,address,address)", + event: "Withdraw", +} // MockERC20 & Masset const underlyingToCredits = (amount: BN | number, exchangeRate: BN): BN => BN.from(amount).mul(fullScale).div(exchangeRate).add(1) @@ -234,6 +251,34 @@ describe("SavingsContract", async () => { const mockSavingsManager = await (await new MockSavingsManager__factory(sa.default.signer)).deploy(savingsContract.address) await nexus.setSavingsManager(mockSavingsManager.address) } + const depositToSenderFn = + (contractFn) => + (contract: SavingsContract) => + (...args) => { + if (contractFn.type === ContractFns.DEPOSIT_SAVINGS) { + return contract[contractFn.fn](...args) + } + return contract[contractFn.fn](...args, contract.signer.getAddress()) + } + const redeemToSenderFn = + (contractFn) => + (contract: SavingsContract) => + (...args) => { + if (contractFn.type === ContractFns.REDEEM_CREDITS) { + return contract[contractFn.fn](...args) + } + return contract[contractFn.fn](...args, contract.signer.getAddress(), contract.signer.getAddress()) + } + + const withdrawToSenderFn = + (contractFn) => + (contract: SavingsContract) => + (...args) => { + if (contractFn.type === ContractFns.REDEEM_UNDERLYING) { + return contract[contractFn.fn](...args) + } + return contract[contractFn.fn](...args, contract.signer.getAddress(), contract.signer.getAddress()) + } before(async () => { const accounts = await ethers.getSigners() @@ -414,29 +459,22 @@ describe("SavingsContract", async () => { }) }) - async function validateDeposits(depositFN: ContractFnType) { - const depositToSender = - (contract: SavingsContract) => - (...args) => { - if (depositFN.type === ContractFns.DEPOSIT_SAVINGS) { - return contract[depositFN.fn](...args) - } - return contract[depositFN.fn](...args, contract.signer.getAddress()) - } + async function validateDeposits(contractFn: ContractFnType) { + const depositToSender = depositToSenderFn(contractFn) async function expectToEmitDepositEvent(tx, sender: string, receiver: string, depositAmount: BN, expectedCredits: BN) { - switch (depositFN.type) { + switch (contractFn.type) { + case ContractFns.DEPOSIT_SAVINGS: + await expect(tx).to.emit(savingsContract, contractFn.event).withArgs(receiver, depositAmount, expectedCredits) + break case ContractFns.DEPOSIT: + default: await expect(tx) - .to.emit(savingsContract, depositFN.event) + .to.emit(savingsContract, contractFn.event) .withArgs(sender, receiver, depositAmount, expectedCredits) break - case ContractFns.DEPOSIT_SAVINGS: - default: - await expect(tx).to.emit(savingsContract, depositFN.event).withArgs(receiver, depositAmount, expectedCredits) - break } } - context(`using ${depositFN.name}`, async () => { + describe(`using ${contractFn.name}`, async () => { before(async () => { await createNewSavingsContract() }) @@ -448,7 +486,7 @@ describe("SavingsContract", async () => { await expect(depositToSender(savingsContract)(ZERO)).to.be.revertedWith("Must deposit something") }) it("should fail when beneficiary is 0", async () => { - await expect(savingsContract[depositFN.fnReceiver](1, ZERO_ADDRESS)).to.be.revertedWith("Invalid beneficiary address") + await expect(savingsContract[contractFn.fnReceiver](1, ZERO_ADDRESS)).to.be.revertedWith("Invalid beneficiary address") }) it("should fail if the user has no balance", async () => { // Approve first @@ -482,7 +520,7 @@ describe("SavingsContract", async () => { await masset.approve(savingsContract.address, depositAmount) - const tx = savingsContract.connect(alice.signer)[depositFN.fnReceiver](depositAmount, bob.address) + const tx = savingsContract.connect(alice.signer)[contractFn.fnReceiver](depositAmount, bob.address) const expectedCredits = underlyingToCredits(depositAmount, initialExchangeRate) await expectToEmitDepositEvent(tx, alice.address, bob.address, depositAmount, expectedCredits) const dataAfter = await getData(savingsContract, bob) @@ -497,7 +535,7 @@ describe("SavingsContract", async () => { await masset.approve(savingsContract.address, depositAmount) - const tx = savingsContract.connect(alice.signer)[depositFN.fnReferrer](depositAmount, bob.address, charlie.address) + const tx = savingsContract.connect(alice.signer)[contractFn.fnReferrer](depositAmount, bob.address, charlie.address) await expect(tx).to.emit(savingsContract, "Referral").withArgs(charlie.address, bob.address, depositAmount) const expectedCredits = underlyingToCredits(depositAmount, initialExchangeRate) @@ -647,30 +685,89 @@ describe("SavingsContract", async () => { expect(await savingsContract.creditsToUnderlying(uToC4)).eq(BN.from(12986123876)) }) }) + describe("minting", async () => { + const contractFn = mint4626Fn + const mintToSender = + (contract: SavingsContract) => + (...args) => + contract[contractFn.fn](...args, contract.signer.getAddress()) + + async function expectToEmitDepositEvent(tx, sender: string, receiver: string, shares: BN, credits: BN) { + await expect(tx).to.emit(savingsContract, contractFn.event).withArgs(sender, receiver, shares, credits) + } + before(async () => { + await createNewSavingsContract() + }) + afterEach(async () => { + const data = await getData(savingsContract, alice) + expect(exchangeRateHolds(data), "Exchange rate must hold") + }) + it("should fail when amount is zero", async () => { + await expect(mintToSender(savingsContract)(ZERO)).to.be.revertedWith("Must deposit something") + }) + it("should fail when beneficiary is 0", async () => { + await expect(savingsContract[contractFn.fnReceiver](10, ZERO_ADDRESS)).to.be.revertedWith("Invalid beneficiary address") + }) + it("should fail if the user has no balance", async () => { + // Approve first + await masset.connect(sa.dummy1.signer).approve(savingsContract.address, simpleToExactAmount(1, 18)) + + // Deposit + await expect(mintToSender(savingsContract.connect(sa.dummy1.signer))(simpleToExactAmount(1, 18))).to.be.revertedWith( + "VM Exception", + ) + }) + it("should deposit the mUSD and assign credits to the saver", async () => { + const dataBefore = await getData(savingsContract, sa.default) + let shares = simpleToExactAmount(10, 18) + const assets = creditsToUnderlying(shares, initialExchangeRate) + // emulate decimals in the smart contract + shares = underlyingToCredits(assets, initialExchangeRate) + + // 1. Approve the savings contract to spend mUSD + await masset.approve(savingsContract.address, assets) + // 2. Deposit the mUSD + const tx = mintToSender(savingsContract)(shares) + await expectToEmitDepositEvent(tx, sa.default.address, sa.default.address, assets, shares) + const dataAfter = await getData(savingsContract, sa.default) + expect(dataAfter.balances.userCredits).eq(shares, "Must receive some savings credits") + expect(dataAfter.balances.totalCredits).eq(shares) + expect(dataAfter.balances.user).eq(dataBefore.balances.user.sub(assets)) + expect(dataAfter.balances.contract).eq(assets) + }) + it("allows alice to deposit to beneficiary (bob.address)", async () => { + const dataBefore = await getData(savingsContract, bob) + let shares = simpleToExactAmount(10, 18) + const assets = creditsToUnderlying(shares, initialExchangeRate) + // emulate decimals in the smart contract + shares = underlyingToCredits(assets, initialExchangeRate) + + await masset.approve(savingsContract.address, assets) + + const tx = savingsContract.connect(alice.signer)[contractFn.fnReceiver](shares, bob.address) + await expectToEmitDepositEvent(tx, alice.address, bob.address, assets, shares) + const dataAfter = await getData(savingsContract, bob) + expect(dataAfter.balances.userCredits).eq(shares, "Must receive some savings credits") + expect(dataAfter.balances.totalCredits).eq(shares.mul(2)) + expect(dataAfter.balances.user).eq(dataBefore.balances.user) + expect(dataAfter.balances.contract).eq(dataBefore.balances.contract.add(assets)) + }) + }) describe("redeeming", async () => { - async function validateRedeems(contractFn:ContractFnType){ - const redeemToSender = - (contract: SavingsContract) => - (...args) => { - if (contractFn.type === ContractFns.REDEEM_CREDITS) { - return contract[contractFn.fn](...args) - } - return contract[contractFn.fn](...args, contract.signer.getAddress(),contract.signer.getAddress()) + async function validateRedeems(contractFn: ContractFnType) { + const redeemToSender = redeemToSenderFn(contractFn) + async function expectToEmitRedeemEvent(tx, sender: string, receiver: string, assets: BN, shares: BN) { + switch (contractFn.type) { + case ContractFns.REDEEM: + await expect(tx).to.emit(savingsContract, contractFn.event).withArgs(sender, receiver, receiver, assets, shares) + break + case ContractFns.REDEEM_CREDITS: + default: + await expect(tx).to.emit(savingsContract, contractFn.event).withArgs(sender, shares, assets) + break } - async function expectToEmitRedeemEvent(tx, sender: string, receiver: string, assets: BN, shares: BN) { - switch (contractFn.type) { - case ContractFns.REDEEM: - await expect(tx) - .to.emit(savingsContract, contractFn.event) - .withArgs(sender, receiver,receiver, assets, shares) - break - case ContractFns.REDEEM_CREDITS: - default: - await expect(tx).to.emit(savingsContract, contractFn.event).withArgs(sender, shares, assets) - break - } - } - context(`scenario ${contractFn.name}`, async () => { + } + describe(`scenario ${contractFn.name}`, async () => { before(async () => { await createNewSavingsContract() }) @@ -681,7 +778,7 @@ describe("SavingsContract", async () => { const amt = BN.from(10) await expect(redeemToSender(savingsContract.connect(sa.other.signer))(amt)).to.be.revertedWith("VM Exception") }) - context(`using ${contractFn.name}`, async () => { + describe(`using ${contractFn.name}`, async () => { const deposit = simpleToExactAmount(10, 18) const credits = underlyingToCredits(deposit, initialExchangeRate) const interest = simpleToExactAmount(10, 18) @@ -718,9 +815,9 @@ describe("SavingsContract", async () => { const dataBefore = await getData(savingsContract, alice) // Enable automateInterestCollection await savingsContract.connect(sa.governor.signer).automateInterestCollectionFlag(true) - + const tx = redeemToSender(savingsContract)(creditsToWithdraw) - await expectToEmitRedeemEvent(tx, alice.address, alice.address, expectedWithdrawal, creditsToWithdraw) + await expectToEmitRedeemEvent(tx, alice.address, alice.address, expectedWithdrawal, creditsToWithdraw) // await tx.wait() const dataAfter = await getData(savingsContract, alice) // burns credits from sender @@ -751,15 +848,15 @@ describe("SavingsContract", async () => { const connector = await ( await new MockConnector__factory(sa.default.signer) ).deploy(savingsContract.address, masset.address) - + await masset.approve(savingsContract.address, simpleToExactAmount(1, 21)) await savingsContract.preDeposit(deposit, alice.address) - + await savingsContract.connect(sa.governor.signer).setConnector(connector.address) - + await ethers.provider.send("evm_increaseTime", [ONE_HOUR.mul(4).add(1).toNumber()]) await ethers.provider.send("evm_mine", []) - + const data = await getData(savingsContract, alice) expect(data.connector.balance).eq(deposit.mul(data.connector.fraction).div(fullScale)) expect(data.balances.contract).eq(deposit.sub(data.connector.balance)) @@ -773,12 +870,12 @@ describe("SavingsContract", async () => { // in order to reach 40%, must redeem > 51 const dataBefore = await getData(savingsContract, alice) const poke = await getExpectedPoke(dataBefore, redemption) - + const tx = redeemToSender(savingsContract)(redemption) - await expectToEmitRedeemEvent(tx, alice.address, alice.address, simpleToExactAmount(51, 18), redemption) + await expectToEmitRedeemEvent(tx, alice.address, alice.address, simpleToExactAmount(51, 18), redemption) // Remaining balance is 49, with 20 in the connector await expect(tx).to.emit(savingsContract, "Poked").withArgs(dataBefore.connector.balance, poke.ideal, BN.from(0)) - + const dataAfter = await getData(savingsContract, alice) expect(dataAfter.balances.contract).eq(simpleToExactAmount("39.2", 18)) }) @@ -798,7 +895,7 @@ describe("SavingsContract", async () => { it("should fail when user doesn't have credits", async () => { const amt = BN.from(10) await expect(savingsContract.connect(sa.other.signer)["redeem(uint256)"](amt)).to.be.revertedWith("VM Exception") - }) + }) it("should redeem when user has balance", async () => { const redemptionAmount = simpleToExactAmount(5, 18) const balancesBefore = await getData(savingsContract, sa.default) @@ -824,83 +921,105 @@ describe("SavingsContract", async () => { expect(creditsAfter, "Must burn all the credits").eq(BN.from(0)) expect(mUSDAfter, "Must receive back mUSD").eq(mUSDBefore.add(redemptionAmount)) }) - }) + }) describe("redeems", async () => { // V1,V2,V3 - await validateRedeems(redeemCreditsFn); + await validateRedeems(redeemCreditsFn) // ERC4626 - await validateRedeems(redeem4626Fn); - }) + await validateRedeems(redeem4626Fn) + }) }) describe("withdrawing", async () => { - before(async () => { - await createNewSavingsContract() - }) - it("should fail when input is zero", async () => { - await expect(savingsContract.redeemUnderlying(ZERO)).to.be.revertedWith("Must withdraw something") - }) - it("should fail when user doesn't have credits", async () => { - const amt = BN.from(10) - await expect(savingsContract.connect(sa.other.signer).redeemUnderlying(amt)).to.be.revertedWith("VM Exception") - }) - context("using redeemUnderlying", async () => { - const deposit = simpleToExactAmount(10, 18) - const interest = simpleToExactAmount(10, 18) - beforeEach(async () => { - await createNewSavingsContract() - await masset.approve(savingsContract.address, simpleToExactAmount(1, 21)) - await savingsContract.preDeposit(deposit, alice.address) - }) - afterEach(async () => { - const data = await getData(savingsContract, alice) - expect(exchangeRateHolds(data), "Exchange rate must hold") - }) - it("allows full redemption immediately after deposit", async () => { - await savingsContract.redeemUnderlying(deposit) - const data = await getData(savingsContract, alice) - expect(data.balances.userCredits).eq(BN.from(0)) - }) - it("should redeem a specific amount of underlying", async () => { - // calculates underlying/credits - const underlying = simpleToExactAmount(5, 18) - const expectedCredits = underlyingToCredits(underlying, initialExchangeRate) - const dataBefore = await getData(savingsContract, alice) - const tx = savingsContract.redeemUnderlying(underlying) - await expect(tx).to.emit(savingsContract, "CreditsRedeemed").withArgs(alice.address, expectedCredits, underlying) - const dataAfter = await getData(savingsContract, alice) - // burns credits from sender - expect(dataAfter.balances.userCredits).eq(dataBefore.balances.userCredits.sub(expectedCredits)) - expect(dataAfter.balances.totalCredits).eq(dataBefore.balances.totalCredits.sub(expectedCredits)) - // transfers tokens to sender - expect(dataAfter.balances.user).eq(dataBefore.balances.user.add(underlying)) - expect(dataAfter.balances.contract).eq(dataBefore.balances.contract.sub(underlying)) - }) - it("collects interest and credits to saver before redemption", async () => { - const expectedExchangeRate = simpleToExactAmount(2, 17) - await masset.setAmountForCollectInterest(interest) - - const dataBefore = await getData(savingsContract, alice) - await savingsContract.redeemUnderlying(deposit) - const dataAfter = await getData(savingsContract, alice) - - expect(dataAfter.balances.user).eq(dataBefore.balances.user.add(deposit)) - // User is left with resulting credits due to exchange rate going up - assertBNClose(dataAfter.balances.userCredits, dataBefore.balances.userCredits.div(2), 1000) - // Exchange rate updates - expect(dataAfter.exchangeRate).eq(expectedExchangeRate) - }) - it("skips interest collection if automate is turned off", async () => { - await masset.setAmountForCollectInterest(interest) - await savingsContract.connect(sa.governor.signer).automateInterestCollectionFlag(false) - - const dataBefore = await getData(savingsContract, alice) - await savingsContract.redeemUnderlying(deposit) - const dataAfter = await getData(savingsContract, alice) - - expect(dataAfter.balances.user).eq(dataBefore.balances.user.add(deposit)) - expect(dataAfter.balances.userCredits).eq(BN.from(0)) - expect(dataAfter.exchangeRate).eq(dataBefore.exchangeRate) + async function validateWithdraws(contractFn: ContractFnType) { + const withdrawToSender = withdrawToSenderFn(contractFn) + async function expectToEmitWithdrawEvent(tx, sender: string, receiver: string, assets: BN, shares: BN) { + switch (contractFn.type) { + case ContractFns.REDEEM_UNDERLYING: + await expect(tx).to.emit(savingsContract, contractFn.event).withArgs(sender, shares, assets) + break + case ContractFns.WITHDRAW: + default: + await expect(tx).to.emit(savingsContract, contractFn.event).withArgs(sender, receiver, sender, assets, shares) + break + } + } + describe(`using ${contractFn.name}`, async () => { + const deposit = simpleToExactAmount(10, 18) + const interest = simpleToExactAmount(10, 18) + beforeEach(async () => { + await createNewSavingsContract() + await masset.approve(savingsContract.address, simpleToExactAmount(1, 21)) + await savingsContract.preDeposit(deposit, alice.address) + }) + afterEach(async () => { + const data = await getData(savingsContract, alice) + expect(exchangeRateHolds(data), "Exchange rate must hold") + }) + it("should fail when input is zero", async () => { + await expect(withdrawToSender(savingsContract)(ZERO)).to.be.revertedWith("Must withdraw something") + }) + it("should fail when user doesn't have credits", async () => { + const amt = BN.from(10) + await expect(withdrawToSender(savingsContract.connect(sa.other.signer))(amt)).to.be.revertedWith("VM Exception") + }) + it("allows full redemption immediately after deposit", async () => { + await withdrawToSender(savingsContract)(deposit) + const data = await getData(savingsContract, alice) + expect(data.balances.userCredits).eq(BN.from(0)) + }) + it("should redeem a specific amount of underlying", async () => { + // calculates underlying/credits + const underlying = simpleToExactAmount(5, 18) + const expectedCredits = underlyingToCredits(underlying, initialExchangeRate) + const dataBefore = await getData(savingsContract, alice) + const tx = withdrawToSender(savingsContract)(underlying) + await expectToEmitWithdrawEvent(tx, alice.address, alice.address, underlying, expectedCredits) + const dataAfter = await getData(savingsContract, alice) + // burns credits from sender + expect(dataAfter.balances.userCredits).eq(dataBefore.balances.userCredits.sub(expectedCredits)) + expect(dataAfter.balances.totalCredits).eq(dataBefore.balances.totalCredits.sub(expectedCredits)) + // transfers tokens to sender + expect(dataAfter.balances.user).eq(dataBefore.balances.user.add(underlying)) + expect(dataAfter.balances.contract).eq(dataBefore.balances.contract.sub(underlying)) + }) + it("collects interest and credits to saver before redemption", async () => { + const expectedExchangeRate = simpleToExactAmount(2, 17) + await masset.setAmountForCollectInterest(interest) + + const dataBefore = await getData(savingsContract, alice) + const expectedCredits = underlyingToCredits(deposit, dataBefore.exchangeRate) + const tx = await withdrawToSender(savingsContract)(deposit) + // console.log("===expectToEmitWithdrawEvent==",alice.address, deposit.toString(), expectedCredits.toString() ) + // await expectToEmitWithdrawEvent(tx, alice.address, alice.address, deposit, expectedCredits) + const dataAfter = await getData(savingsContract, alice) + + expect(dataAfter.balances.user).eq(dataBefore.balances.user.add(deposit)) + // User is left with resulting credits due to exchange rate going up + assertBNClose(dataAfter.balances.userCredits, dataBefore.balances.userCredits.div(2), 1000) + // Exchange rate updates + expect(dataAfter.exchangeRate).eq(expectedExchangeRate) + }) + it("skips interest collection if automate is turned off", async () => { + await masset.setAmountForCollectInterest(interest) + await savingsContract.connect(sa.governor.signer).automateInterestCollectionFlag(false) + + const dataBefore = await getData(savingsContract, alice) + const tx = await withdrawToSender(savingsContract)(deposit) + const expectedCredits = underlyingToCredits(deposit, initialExchangeRate) + await expectToEmitWithdrawEvent(tx, alice.address, alice.address, deposit, expectedCredits) + const dataAfter = await getData(savingsContract, alice) + + expect(dataAfter.balances.user).eq(dataBefore.balances.user.add(deposit)) + expect(dataAfter.balances.userCredits).eq(BN.from(0)) + expect(dataAfter.exchangeRate).eq(dataBefore.exchangeRate) + }) }) + } + describe("withdraws", async () => { + // V1,V2,V3 + await validateWithdraws(redeemUnderlyingFn) + // ERC4626 + await validateWithdraws(withdraw4626Fn) }) }) @@ -1030,33 +1149,36 @@ describe("SavingsContract", async () => { const data = await getData(savingsContract, alice) expect(exchangeRateHolds(data), "Exchange rate must hold") }) - it("should fail if the raw balance goes down somehow", async () => { - const connector = await ( - await new MockErroneousConnector1__factory(sa.default.signer) - ).deploy(savingsContract.address, masset.address) - await savingsContract.connect(sa.governor.signer).setConnector(connector.address) - // Total collat goes down - await savingsContract.redeemUnderlying(deposit.div(2)) - // Withdrawal is made but nothing comes back - await ethers.provider.send("evm_increaseTime", [ONE_HOUR.mul(6).toNumber()]) - await ethers.provider.send("evm_mine", []) - await savingsContract.poke() - // Try that again - await ethers.provider.send("evm_increaseTime", [ONE_HOUR.mul(12).toNumber()]) - await ethers.provider.send("evm_mine", []) - await expect(savingsContract.poke()).to.be.revertedWith("ExchangeRate must increase") - }) - it("is protected by the system invariant", async () => { - // connector returns invalid balance after withdrawal - const connector = await ( - await new MockErroneousConnector2__factory(sa.default.signer) - ).deploy(savingsContract.address, masset.address) - await savingsContract.connect(sa.governor.signer).setConnector(connector.address) - await savingsContract.redeemUnderlying(deposit.div(2)) - - await ethers.provider.send("evm_increaseTime", [ONE_HOUR.mul(4).toNumber()]) - await ethers.provider.send("evm_mine", []) - await expect(savingsContract.poke()).to.be.revertedWith("Enforce system invariant") + ;[redeemUnderlyingFn, withdraw4626Fn].forEach((fn) => { + const withdrawToSender = withdrawToSenderFn(fn) + it(`${fn.name} should fail if the raw balance goes down somehow`, async () => { + const connector = await ( + await new MockErroneousConnector1__factory(sa.default.signer) + ).deploy(savingsContract.address, masset.address) + await savingsContract.connect(sa.governor.signer).setConnector(connector.address) + // Total collat goes down + await withdrawToSender(savingsContract)(deposit.div(2)) + // Withdrawal is made but nothing comes back + await ethers.provider.send("evm_increaseTime", [ONE_HOUR.mul(6).toNumber()]) + await ethers.provider.send("evm_mine", []) + await savingsContract.poke() + // Try that again + await ethers.provider.send("evm_increaseTime", [ONE_HOUR.mul(12).toNumber()]) + await ethers.provider.send("evm_mine", []) + await expect(savingsContract.poke()).to.be.revertedWith("ExchangeRate must increase") + }) + it(`${fn.name} is protected by the system invariant`, async () => { + // connector returns invalid balance after withdrawal + const connector = await ( + await new MockErroneousConnector2__factory(sa.default.signer) + ).deploy(savingsContract.address, masset.address) + await savingsContract.connect(sa.governor.signer).setConnector(connector.address) + await withdrawToSender(savingsContract)(deposit.div(2)) + + await ethers.provider.send("evm_increaseTime", [ONE_HOUR.mul(4).toNumber()]) + await ethers.provider.send("evm_mine", []) + await expect(savingsContract.poke()).to.be.revertedWith("Enforce system invariant") + }) }) it("should fail if the balance has gone down", async () => { const connector = await ( @@ -1389,86 +1511,97 @@ describe("SavingsContract", async () => { }) }) - context("performing multiple operations from multiple addresses in sequence", async () => { - beforeEach(async () => { - await createNewSavingsContract() - }) + ;[ + { deposit: depositSavingsFn, redeem: redeemCreditsFn }, + { deposit: deposit4626Fn, redeem: redeem4626Fn }, + ].forEach((fn, index) => { + const depositToSender = depositToSenderFn(fn.deposit) + const redeemToSender = redeemToSenderFn(fn.redeem) + context(`performing multiple operations from multiple addresses in sequence - ${index}`, async () => { + beforeEach(async () => { + await createNewSavingsContract() + }) - it("should give existing savers the benefit of the increased exchange rate", async () => { - const saver1 = sa.default - const saver2 = sa.dummy1 - const saver3 = sa.dummy2 - const saver4 = sa.dummy3 - - // Set up amounts - // Each savers deposit will trigger some interest to be deposited - const saver1deposit = simpleToExactAmount(1000, 18) - const interestToReceive1 = simpleToExactAmount(100, 18) - const saver2deposit = simpleToExactAmount(1000, 18) - const interestToReceive2 = simpleToExactAmount(350, 18) - const saver3deposit = simpleToExactAmount(1000, 18) - const interestToReceive3 = simpleToExactAmount(80, 18) - const saver4deposit = simpleToExactAmount(1000, 18) - const interestToReceive4 = simpleToExactAmount(160, 18) - - // Ensure saver2 has some balances and do approvals - await masset.transfer(saver2.address, saver2deposit) - await masset.transfer(saver3.address, saver3deposit) - await masset.transfer(saver4.address, saver4deposit) - await masset.connect(saver1.signer).approve(savingsContract.address, MAX_UINT256) - await masset.connect(saver2.signer).approve(savingsContract.address, MAX_UINT256) - await masset.connect(saver3.signer).approve(savingsContract.address, MAX_UINT256) - await masset.connect(saver4.signer).approve(savingsContract.address, MAX_UINT256) - - // Should be a fresh balance sheet - const stateBefore = await getData(savingsContract, sa.default) - expect(stateBefore.exchangeRate).to.equal(initialExchangeRate) - expect(stateBefore.balances.contract).to.equal(BN.from(0)) - - // 1.0 user 1 deposits - // interest remains unassigned and exchange rate unmoved - await masset.setAmountForCollectInterest(interestToReceive1) - - await ethers.provider.send("evm_increaseTime", [ONE_DAY.toNumber()]) - await ethers.provider.send("evm_mine", []) - await savingsContract.connect(saver1.signer)["depositSavings(uint256)"](saver1deposit) - await savingsContract.poke() - const state1 = await getData(savingsContract, saver1) - // 2.0 user 2 deposits - // interest rate benefits user 1 and issued user 2 less credits than desired - await masset.setAmountForCollectInterest(interestToReceive2) + it("should give existing savers the benefit of the increased exchange rate", async () => { + const saver1 = sa.default + const saver2 = sa.dummy1 + const saver3 = sa.dummy2 + const saver4 = sa.dummy3 + + // Set up amounts + // Each savers deposit will trigger some interest to be deposited + const saver1deposit = simpleToExactAmount(1000, 18) + const interestToReceive1 = simpleToExactAmount(100, 18) + const saver2deposit = simpleToExactAmount(1000, 18) + const interestToReceive2 = simpleToExactAmount(350, 18) + const saver3deposit = simpleToExactAmount(1000, 18) + const interestToReceive3 = simpleToExactAmount(80, 18) + const saver4deposit = simpleToExactAmount(1000, 18) + const interestToReceive4 = simpleToExactAmount(160, 18) + + // Ensure saver2 has some balances and do approvals + await masset.transfer(saver2.address, saver2deposit) + await masset.transfer(saver3.address, saver3deposit) + await masset.transfer(saver4.address, saver4deposit) + await masset.connect(saver1.signer).approve(savingsContract.address, MAX_UINT256) + await masset.connect(saver2.signer).approve(savingsContract.address, MAX_UINT256) + await masset.connect(saver3.signer).approve(savingsContract.address, MAX_UINT256) + await masset.connect(saver4.signer).approve(savingsContract.address, MAX_UINT256) + + // Should be a fresh balance sheet + const stateBefore = await getData(savingsContract, sa.default) + expect(stateBefore.exchangeRate).to.equal(initialExchangeRate) + expect(stateBefore.balances.contract).to.equal(BN.from(0)) + + // 1.0 user 1 deposits + // interest remains unassigned and exchange rate unmoved + await masset.setAmountForCollectInterest(interestToReceive1) - await ethers.provider.send("evm_increaseTime", [ONE_DAY.toNumber()]) - await ethers.provider.send("evm_mine", []) - await savingsContract.connect(saver2.signer)["depositSavings(uint256)"](saver2deposit) - const state2 = await getData(savingsContract, saver2) - // 3.0 user 3 deposits - // interest rate benefits users 1 and 2 - await masset.setAmountForCollectInterest(interestToReceive3) + await ethers.provider.send("evm_increaseTime", [ONE_DAY.toNumber()]) + await ethers.provider.send("evm_mine", []) + await depositToSender(savingsContract.connect(saver1.signer))(saver1deposit) + await savingsContract.poke() + const state1 = await getData(savingsContract, saver1) + // 2.0 user 2 deposits + // interest rate benefits user 1 and issued user 2 less credits than desired + await masset.setAmountForCollectInterest(interestToReceive2) - await ethers.provider.send("evm_increaseTime", [ONE_DAY.toNumber()]) - await ethers.provider.send("evm_mine", []) - await savingsContract.connect(saver3.signer)["depositSavings(uint256)"](saver3deposit) - const state3 = await getData(savingsContract, saver3) - // 4.0 user 1 withdraws all her credits - await savingsContract.connect(saver1.signer)["redeem(uint256)"](state1.balances.userCredits) - const state4 = await getData(savingsContract, saver1) - expect(state4.balances.userCredits).eq(BN.from(0)) - expect(state4.balances.totalCredits).eq(state3.balances.totalCredits.sub(state1.balances.userCredits)) - expect(state4.exchangeRate).eq(state3.exchangeRate) - assertBNClose(state4.balances.contract, creditsToUnderlying(state4.balances.totalCredits, state4.exchangeRate), BN.from(100000)) - // 5.0 user 4 deposits - // interest rate benefits users 2 and 3 - await masset.setAmountForCollectInterest(interestToReceive4) - - await ethers.provider.send("evm_increaseTime", [ONE_DAY.toNumber()]) - await ethers.provider.send("evm_mine", []) - await savingsContract.connect(saver4.signer)["depositSavings(uint256)"](saver4deposit) - const state5 = await getData(savingsContract, saver4) - // 6.0 users 2, 3, and 4 withdraw all their tokens - await savingsContract.connect(saver2.signer).redeemCredits(state2.balances.userCredits) - await savingsContract.connect(saver3.signer).redeemCredits(state3.balances.userCredits) - await savingsContract.connect(saver4.signer).redeemCredits(state5.balances.userCredits) + await ethers.provider.send("evm_increaseTime", [ONE_DAY.toNumber()]) + await ethers.provider.send("evm_mine", []) + await depositToSender(savingsContract.connect(saver2.signer))(saver2deposit) + const state2 = await getData(savingsContract, saver2) + // 3.0 user 3 deposits + // interest rate benefits users 1 and 2 + await masset.setAmountForCollectInterest(interestToReceive3) + + await ethers.provider.send("evm_increaseTime", [ONE_DAY.toNumber()]) + await ethers.provider.send("evm_mine", []) + await depositToSender(savingsContract.connect(saver3.signer))(saver3deposit) + const state3 = await getData(savingsContract, saver3) + // 4.0 user 1 withdraws all her credits + await redeemToSender(savingsContract.connect(saver1.signer))(state1.balances.userCredits) + const state4 = await getData(savingsContract, saver1) + expect(state4.balances.userCredits).eq(BN.from(0)) + expect(state4.balances.totalCredits).eq(state3.balances.totalCredits.sub(state1.balances.userCredits)) + expect(state4.exchangeRate).eq(state3.exchangeRate) + assertBNClose( + state4.balances.contract, + creditsToUnderlying(state4.balances.totalCredits, state4.exchangeRate), + BN.from(100000), + ) + // 5.0 user 4 deposits + // interest rate benefits users 2 and 3 + await masset.setAmountForCollectInterest(interestToReceive4) + + await ethers.provider.send("evm_increaseTime", [ONE_DAY.toNumber()]) + await ethers.provider.send("evm_mine", []) + await depositToSender(savingsContract.connect(saver4.signer))(saver4deposit) + const state5 = await getData(savingsContract, saver4) + // 6.0 users 2, 3, and 4 withdraw all their tokens + await redeemToSender(savingsContract.connect(saver2.signer))(state2.balances.userCredits) + await redeemToSender(savingsContract.connect(saver3.signer))(state3.balances.userCredits) + await redeemToSender(savingsContract.connect(saver4.signer))(state5.balances.userCredits) + }) }) }) }) From cdd2fe6661e7f80538f5fdbc6d8ea1fc4d2ccc59 Mon Sep 17 00:00:00 2001 From: doncesarts Date: Wed, 13 Apr 2022 00:17:59 +0100 Subject: [PATCH 07/14] test: adds test shouldBehaveLikeERC4626 --- contracts/interfaces/IERC4626Vault.sol | 2 +- contracts/savings/SavingsContract.sol | 23 +- .../savings/sc22-mainnet-upgrade.spec.ts | 505 ++++++++++++++++++ .../savings/sc22-polygon-upgrade.spec.ts | 427 +++++++++++++++ test/savings/savings-contract.spec.ts | 26 +- test/shared/ERC4626.behaviour.ts | 328 ++++++++++++ test/shared/ERC4626.behaviours.ts | 173 ------ 7 files changed, 1283 insertions(+), 201 deletions(-) create mode 100644 test-fork/savings/sc22-mainnet-upgrade.spec.ts create mode 100644 test-fork/savings/sc22-polygon-upgrade.spec.ts create mode 100644 test/shared/ERC4626.behaviour.ts delete mode 100644 test/shared/ERC4626.behaviours.ts diff --git a/contracts/interfaces/IERC4626Vault.sol b/contracts/interfaces/IERC4626Vault.sol index 7555f559..70289c41 100644 --- a/contracts/interfaces/IERC4626Vault.sol +++ b/contracts/interfaces/IERC4626Vault.sol @@ -10,7 +10,7 @@ import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; * @dev VERSION: 1.0 * DATE: 2022-02-10 */ -interface IERC4626Vault { +interface IERC4626Vault is IERC20 { /// @notice The address of the underlying token used for the Vault uses for accounting, depositing, and withdrawing function asset() external view returns (address assetTokenAddress); diff --git a/contracts/savings/SavingsContract.sol b/contracts/savings/SavingsContract.sol index 34fddf3d..f0dc2f1e 100644 --- a/contracts/savings/SavingsContract.sol +++ b/contracts/savings/SavingsContract.sol @@ -806,7 +806,6 @@ contract SavingsContract is */ function deposit(uint256 assets, address receiver) external override returns (uint256 shares) { shares = _transferAndMint(assets, receiver, true); - emit Deposit(msg.sender, receiver, assets, shares); } /** @@ -824,16 +823,15 @@ contract SavingsContract is ) external returns (uint256 shares) { shares = _transferAndMint(assets, receiver, true); emit Referral(referrer, receiver, assets); - emit Deposit(msg.sender, receiver, assets, shares); } /** * @notice The maximum number of vault shares that caller can mint. - * @param caller Account that the underlying assets will be transferred from. + * caller Account that the underlying assets will be transferred from. * @return maxShares The maximum amount of vault shares the caller can mint. */ - function maxMint(address caller) external view override returns (uint256 maxShares) { - maxShares = balanceOf(caller); + function maxMint(address /* caller */) external pure override returns (uint256 maxShares) { + maxShares = type(uint256).max; } /** @@ -856,7 +854,6 @@ contract SavingsContract is function mint(uint256 shares, address receiver) external override returns (uint256 assets) { (assets, ) = _creditsToUnderlying(shares); _transferAndMint(assets, receiver, true); - emit Deposit(msg.sender, receiver, assets, shares); } /** @@ -865,7 +862,6 @@ contract SavingsContract is */ function maxWithdraw(address caller) external view override returns (uint256 maxAssets) { (maxAssets, ) = _creditsToUnderlying(balanceOf(caller)); - return maxAssets; } /** @@ -875,7 +871,6 @@ contract SavingsContract is */ function previewWithdraw(uint256 assets) external view override returns (uint256 shares) { (shares, ) = _underlyingToCredits(assets); - return shares; } /** @@ -896,7 +891,6 @@ contract SavingsContract is (shares, _exchangeRate) = _underlyingToCredits(assets); _burnTransfer(assets, shares, receiver, owner, _exchangeRate, true); - emit Withdraw(msg.sender, receiver, owner, assets, shares); } /** @@ -906,7 +900,6 @@ contract SavingsContract is */ function maxRedeem(address caller) external view override returns (uint256 maxShares) { maxShares = balanceOf(caller); - return maxShares; } /** @@ -938,7 +931,6 @@ contract SavingsContract is (assets, _exchangeRate) = _creditsToUnderlying(shares); _burnTransfer(assets, shares, receiver, owner, _exchangeRate, true); //transferAssets=true - emit Withdraw(msg.sender, receiver, owner, assets, shares); } /*/////////////////////////////////////////////////////////////// @@ -966,6 +958,7 @@ contract SavingsContract is // add credits to ERC20 balances _mint(receiver, shares); + emit Deposit(msg.sender, receiver, assets, shares); } /*/////////////////////////////////////////////////////////////// @@ -983,18 +976,18 @@ contract SavingsContract is // If caller is not the owner of the shares uint256 allowed = allowance(owner, msg.sender); if (msg.sender != owner && allowed != type(uint256).max) { - require(shares <= allowed, "amount exceeds allowance"); + require(shares <= allowed, "Amount exceeds allowance"); _approve(owner, msg.sender, allowed - shares); } - // Burn required shares from the sender FIRST + // Burn required shares from the owner FIRST _burn(owner, shares); - // Optionally, transfer tokens from here to sender + // Optionally, transfer tokens from here to receiver if (transferAssets) { require(underlying.transfer(receiver, assets), "Must send tokens"); + emit Withdraw(msg.sender, receiver, owner, assets, shares); } - // If this withdrawal pushes the portion of stored collateral in the `connector` over a certain // threshold (fraction + 20%), then this should trigger a _poke on the connector. This is to avoid // a situation in which there is a rush on withdrawals for some reason, causing the connector diff --git a/test-fork/savings/sc22-mainnet-upgrade.spec.ts b/test-fork/savings/sc22-mainnet-upgrade.spec.ts new file mode 100644 index 00000000..322d7117 --- /dev/null +++ b/test-fork/savings/sc22-mainnet-upgrade.spec.ts @@ -0,0 +1,505 @@ +import { impersonate } from "@utils/fork" +import { Signer, ContractFactory } from "ethers" +import { expect } from "chai" +import { network } from "hardhat" +import { deployContract } from "tasks/utils/deploy-utils" + +// Mainnet imBTC Vault +import { BoostedSavingsVaultImbtcMainnet2__factory } from "types/generated/factories/BoostedSavingsVaultImbtcMainnet2__factory" +import { BoostedSavingsVaultImbtcMainnet2 } from "types/generated/BoostedSavingsVaultImbtcMainnet2" + +// Mainnet imUSD Vault +import { BoostedSavingsVaultImusdMainnet2__factory } from "types/generated/factories/BoostedSavingsVaultImusdMainnet2__factory" +import { BoostedSavingsVaultImusdMainnet2 } from "types/generated/BoostedSavingsVaultImusdMainnet2" + +// Mainnet imBTC Contract +import { SavingsContractImbtcMainnet22__factory } from "types/generated/factories/SavingsContractImbtcMainnet22__factory" +import { SavingsContractImbtcMainnet22 } from "types/generated/SavingsContractImbtcMainnet22" +// Mainnet imUSD Contract +import { SavingsContractImusdMainnet22__factory } from "types/generated/factories/SavingsContractImusdMainnet22__factory" +import { SavingsContractImusdMainnet22 } from "types/generated/SavingsContractImusdMainnet22" + +import { + DelayedProxyAdmin, + DelayedProxyAdmin__factory, + ERC20__factory, + IERC20__factory, + Unwrapper, + Unwrapper__factory, +} from "types/generated" + +import { assertBNClosePercent, Chain, DEAD_ADDRESS, simpleToExactAmount } from "index" +import { BigNumber } from "@ethersproject/bignumber" +import { getChainAddress, resolveAddress } from "tasks/utils/networkAddressFactory" +import { upgradeContract } from "@utils/deploy" + +const chain = Chain.mainnet +const delayedProxyAdminAddress = getChainAddress("DelayedProxyAdmin", chain) +const governorAddress = getChainAddress("Governor", chain) +const nexusAddress = getChainAddress("Nexus", chain) +const boostDirector = getChainAddress("BoostDirector", chain) + +const deployerAddress = "0x19F12C947D25Ff8a3b748829D8001cA09a28D46d" +const imusdHolderAddress = "0xdA1fD36cfC50ED03ca4dd388858A78C904379fb3" +const musdHolderAddress = "0x8474ddbe98f5aa3179b3b3f5942d724afcdec9f6" +const imbtcHolderAddress = "0x720366c95d26389471c52f854d43292157c03efd" +const vmusdHolderAddress = "0x0c2ef8a1b3bc00bf676053732f31a67ebba5bd81" +const vmbtcHolderAddress = "0x10d96b1fd46ce7ce092aa905274b8ed9d4585a6e" +const vhbtcmbtcHolderAddress = "0x10d96b1fd46ce7ce092aa905274b8ed9d4585a6e" +const daiAddress = resolveAddress("DAI", Chain.mainnet) +const alusdAddress = resolveAddress("alUSD", Chain.mainnet) +const musdAddress = resolveAddress("mUSD", Chain.mainnet) +const imusdAddress = resolveAddress("mUSD", Chain.mainnet, "savings") +const imusdVaultAddress = resolveAddress("mUSD", Chain.mainnet, "vault") +const alusdFeederPool = resolveAddress("alUSD", Chain.mainnet, "feederPool") +const mtaAddress = resolveAddress("MTA", Chain.mainnet) +const mbtcAddress = resolveAddress("mBTC", Chain.mainnet) +const imbtcAddress = resolveAddress("mBTC", Chain.mainnet, "savings") +const imbtcVaultAddress = resolveAddress("mBTC", Chain.mainnet, "vault") +const wbtcAddress = resolveAddress("WBTC", Chain.mainnet) +const hbtcAddress = resolveAddress("HBTC", Chain.mainnet) +const hbtcFeederPool = resolveAddress("HBTC", Chain.mainnet, "feederPool") + +// DEPLOYMENT PIPELINE +// 1. Deploy Unwrapper +// 1.1. Set the Unwrapper address as constant in imUSD Vault via initialize +// 2. Upgrade and check storage +// 2.1. Vaults +// 2.2. SavingsContracts +// 3. Do some unwrapping +// 3.1. Directly to unwrapper +// 3.2. Via SavingsContracts +// 3.3. Via SavingsVaults +context("Unwrapper and Vault upgrades", () => { + let deployer: Signer + let musdHolder: Signer + let unwrapper: Unwrapper + let governor: Signer + let delayedProxyAdmin: DelayedProxyAdmin + + const redeemAndUnwrap = async ( + holderAddress: string, + router: string, + input: "musd" | "mbtc", + outputAddress: string, + isCredit = false, + ) => { + const holder = await impersonate(holderAddress) + const saveAddress = input === "musd" ? imusdAddress : imbtcAddress + let inputAddress = input === "musd" ? musdAddress : mbtcAddress + + if (input === "musd" && isCredit) { + inputAddress = imusdAddress + } else if (input === "musd" && !isCredit) { + inputAddress = musdAddress + } else if (input !== "musd" && isCredit) { + inputAddress = imbtcAddress + } else { + inputAddress = mbtcAddress + } + + const amount = input === "musd" ? simpleToExactAmount(1, 18) : simpleToExactAmount(1, 14) + + const config = { + router, + input: inputAddress, + output: outputAddress, + amount: isCredit ? amount : amount.mul(10), + isCredit, + } + + // Get estimated output via getUnwrapOutput + const isBassetOut = await unwrapper.callStatic.getIsBassetOut(config.input, config.isCredit, config.output) + const amountOut = await unwrapper.getUnwrapOutput( + isBassetOut, + config.router, + config.input, + config.isCredit, + config.output, + config.amount, + ) + expect(amountOut.toString().length).to.be.gte(input === "musd" ? 18 : 4) + const minAmountOut = amountOut.mul(98).div(1e2) + const outContract = IERC20__factory.connect(config.output, holder) + const tokenBalanceBefore = await outContract.balanceOf(holderAddress) + const saveContract = + input === "musd" + ? SavingsContractImusdMainnet22__factory.connect(saveAddress, holder) + : SavingsContractImbtcMainnet22__factory.connect(saveAddress, holder) + + await saveContract.redeemAndUnwrap( + config.amount, + config.isCredit, + minAmountOut, + config.output, + holderAddress, + config.router, + isBassetOut, + ) + + const tokenBalanceAfter = await outContract.balanceOf(holderAddress) + const tokenBalanceDifference = tokenBalanceAfter.sub(tokenBalanceBefore) + assertBNClosePercent(tokenBalanceDifference, amountOut, 0.001) + expect(tokenBalanceAfter, "Token balance has increased").to.be.gt(tokenBalanceBefore) + } + + before("reset block number", async () => { + await network.provider.request({ + method: "hardhat_reset", + params: [ + { + forking: { + jsonRpcUrl: process.env.NODE_URL, + // Nov-25-2021 03:15:21 PM +UTC + blockNumber: 13684204, + }, + }, + ], + }) + musdHolder = await impersonate(musdHolderAddress) + deployer = await impersonate(deployerAddress) + governor = await impersonate(governorAddress) + + delayedProxyAdmin = DelayedProxyAdmin__factory.connect(delayedProxyAdminAddress, governor) + }) + it("Test connectivity", async () => { + const startEther = await deployer.getBalance() + const address = await deployer.getTransactionCount() + console.log(`Deployer ${address} has ${startEther} Ether`) + }) + + context("Stage 1", () => { + it("Deploys the unwrapper proxy contract ", async () => { + unwrapper = await deployContract(new Unwrapper__factory(deployer), "Unwrapper", [nexusAddress]) + expect(unwrapper.address).to.length(42) + + // approve tokens for router + const routers = [alusdFeederPool, hbtcFeederPool] + const tokens = [musdAddress, mbtcAddress] + await unwrapper.connect(governor).approve(routers, tokens) + }) + }) + + context("Stage 2", () => { + describe.skip("2.1 Upgrading vaults", () => { + it("Upgrades the imUSD Vault", async () => { + const saveVaultImpl = await deployContract( + new BoostedSavingsVaultImusdMainnet2__factory(deployer), + "mStable: mUSD Savings Vault", + [], + ) + await upgradeContract( + BoostedSavingsVaultImusdMainnet2__factory as unknown as ContractFactory, + saveVaultImpl, + imusdVaultAddress, + governor, + delayedProxyAdmin, + ) + expect(await delayedProxyAdmin.getProxyImplementation(imusdVaultAddress)).eq(saveVaultImpl.address) + }) + + it("Upgrades the imBTC Vault", async () => { + const priceCoeff = simpleToExactAmount(4800, 18) + const boostCoeff = 9 + + const saveVaultImpl = await deployContract( + new BoostedSavingsVaultImbtcMainnet2__factory(deployer), + "mStable: mBTC Savings Vault", + [nexusAddress, imbtcAddress, boostDirector, priceCoeff, boostCoeff, mtaAddress], + ) + await upgradeContract( + BoostedSavingsVaultImbtcMainnet2__factory as unknown as ContractFactory, + saveVaultImpl, + imbtcVaultAddress, + governor, + delayedProxyAdmin, + ) + expect(await delayedProxyAdmin.getProxyImplementation(imbtcVaultAddress)).eq(saveVaultImpl.address) + }) + }) + describe("2.2 Upgrading savings contracts", () => { + it("Upgrades the imUSD contract", async () => { + const musdSaveImpl = await deployContract( + new SavingsContractImusdMainnet22__factory(deployer), + "mStable: mUSD Savings Contract", + [], + ) + + // const upgradeData = musdSaveImpl.interface.encodeFunctionData("upgradeV3", [unwrapper.address]) + // Method upgradeV3 is for test purposes only + /** + solidity code + function upgradeV3(address _unwrapper) external { + // TODO - REMOVE BEFORE DEPLOYMENT + require(_unwrapper != address(0), "Invalid unwrapper address"); + unwrapper = _unwrapper; + } + */ + + const upgradeData = [] + + const saveContractProxy = await upgradeContract( + SavingsContractImusdMainnet22__factory as unknown as ContractFactory, + musdSaveImpl, + imusdAddress, + governor, + delayedProxyAdmin, + upgradeData, + ) + + const unwrapperAddress = await saveContractProxy.unwrapper() + expect(unwrapperAddress).to.eq(unwrapper.address) + expect(await delayedProxyAdmin.getProxyImplementation(imusdAddress)).eq(musdSaveImpl.address) + expect(musdAddress).eq(await musdSaveImpl.underlying()) + }) + + it("imUSD contract works after upgraded", async () => { + await redeemAndUnwrap(imusdHolderAddress, musdAddress, "musd", daiAddress) + }) + + it("Upgrades the imBTC contract", async () => { + const constructorArguments = [nexusAddress, mbtcAddress, unwrapper.address] + const mbtcSaveImpl = await deployContract( + new SavingsContractImbtcMainnet22__factory(deployer), + "mStable: mBTC Savings", + constructorArguments, + ) + + const saveContractProxy = await upgradeContract( + SavingsContractImbtcMainnet22__factory as unknown as ContractFactory, + mbtcSaveImpl, + imbtcAddress, + governor, + delayedProxyAdmin, + ) + expect(await delayedProxyAdmin.getProxyImplementation(imbtcAddress)).eq(mbtcSaveImpl.address) + const unwrapperAddress = await saveContractProxy.unwrapper() + expect(unwrapperAddress).to.eq(unwrapper.address) + }) + + it("imBTC contract works after upgraded", async () => { + await redeemAndUnwrap(imbtcHolderAddress, mbtcAddress, "mbtc", wbtcAddress) + }) + }) + }) + + context("Stage 3", () => { + describe("3.1 Directly", () => { + it("Can call getIsBassetOut & it functions correctly", async () => { + const isCredit = true + expect(await unwrapper.callStatic.getIsBassetOut(musdAddress, !isCredit, daiAddress)).to.eq(true) + expect(await unwrapper.callStatic.getIsBassetOut(musdAddress, !isCredit, musdAddress)).to.eq(false) + expect(await unwrapper.callStatic.getIsBassetOut(musdAddress, !isCredit, alusdAddress)).to.eq(false) + expect(await unwrapper.callStatic.getIsBassetOut(mbtcAddress, !isCredit, wbtcAddress)).to.eq(true) + expect(await unwrapper.callStatic.getIsBassetOut(mbtcAddress, !isCredit, mbtcAddress)).to.eq(false) + expect(await unwrapper.callStatic.getIsBassetOut(mbtcAddress, !isCredit, hbtcAddress)).to.eq(false) + }) + + const validateAssetRedemption = async ( + config: { + router: string + input: string + output: string + amount: BigNumber + isCredit: boolean + }, + signer: Signer, + ) => { + // Get estimated output via getUnwrapOutput + const signerAddress = await signer.getAddress() + const isBassetOut = await unwrapper.callStatic.getIsBassetOut(config.input, false, config.output) + + const amountOut = await unwrapper.getUnwrapOutput( + isBassetOut, + config.router, + config.input, + config.isCredit, + config.output, + config.amount, + ) + expect(amountOut.toString().length).to.be.gte(18) + const minAmountOut = amountOut.mul(98).div(1e2) + + const newConfig = { + ...config, + minAmountOut, + beneficiary: signerAddress, + } + + // check balance before + const tokenOut = IERC20__factory.connect(config.output, signer) + const tokenBalanceBefore = await tokenOut.balanceOf(signerAddress) + + // approve musd for unwrapping + const tokenInput = IERC20__factory.connect(config.input, signer) + await tokenInput.approve(unwrapper.address, config.amount) + + // redeem to basset via unwrapAndSend + await unwrapper + .connect(signer) + .unwrapAndSend( + isBassetOut, + newConfig.router, + newConfig.input, + newConfig.output, + newConfig.amount, + newConfig.minAmountOut, + newConfig.beneficiary, + ) + + // check balance after + const tokenBalanceAfter = await tokenOut.balanceOf(signerAddress) + expect(tokenBalanceAfter, "Token balance has increased").to.be.gt(tokenBalanceBefore) + } + + it("Receives the correct output from getUnwrapOutput", async () => { + const config = { + router: musdAddress, + input: musdAddress, + output: daiAddress, + amount: simpleToExactAmount(1, 18), + isCredit: false, + } + const isBassetOut = await unwrapper.callStatic.getIsBassetOut(config.input, config.isCredit, config.output) + const output = await unwrapper.getUnwrapOutput( + isBassetOut, + config.router, + config.input, + config.isCredit, + config.output, + config.amount, + ) + expect(output.toString()).to.be.length(19) + }) + + it("mUSD redeem to bAsset via unwrapAndSend", async () => { + const config = { + router: musdAddress, + input: musdAddress, + output: daiAddress, + amount: simpleToExactAmount(1, 18), + isCredit: false, + } + + await validateAssetRedemption(config, musdHolder) + }) + + it("mUSD redeem to fAsset via unwrapAndSend", async () => { + const config = { + router: alusdFeederPool, + input: musdAddress, + output: alusdAddress, + amount: simpleToExactAmount(1, 18), + isCredit: false, + } + await validateAssetRedemption(config, musdHolder) + }) + }) + + describe("3.2 Via SavingsContracts", () => { + it("mUSD contract redeem to bAsset", async () => { + await redeemAndUnwrap(imusdHolderAddress, musdAddress, "musd", daiAddress) + }) + + it("mUSD contract redeem to fAsset", async () => { + await redeemAndUnwrap(imusdHolderAddress, alusdFeederPool, "musd", alusdAddress) + }) + it("mBTC contract redeem to bAsset", async () => { + await redeemAndUnwrap(imbtcHolderAddress, mbtcAddress, "mbtc", wbtcAddress) + }) + + it("mBTC contract redeem to fAsset", async () => { + await redeemAndUnwrap(imbtcHolderAddress, hbtcFeederPool, "mbtc", hbtcAddress) + }) + // credits + it("imUSD contract redeem to bAsset", async () => { + await redeemAndUnwrap(imusdHolderAddress, musdAddress, "musd", daiAddress, true) + }) + + it("imUSD contract redeem to fAsset", async () => { + await redeemAndUnwrap(imusdHolderAddress, alusdFeederPool, "musd", alusdAddress, true) + }) + it("imBTC contract redeem to bAsset", async () => { + await redeemAndUnwrap(imbtcHolderAddress, mbtcAddress, "mbtc", wbtcAddress, true) + }) + + it("imBTC contract redeem to fAsset", async () => { + await redeemAndUnwrap(imbtcHolderAddress, hbtcFeederPool, "mbtc", hbtcAddress, true) + }) + }) + + describe("3.3 Via Vaults", () => { + const withdrawAndUnwrap = async (holderAddress: string, router: string, input: "musd" | "mbtc", outputAddress: string) => { + const isCredit = true + const holder = await impersonate(holderAddress) + const vaultAddress = input === "musd" ? imusdVaultAddress : imbtcVaultAddress + const inputAddress = input === "musd" ? imusdAddress : imbtcAddress + const isBassetOut = await unwrapper.callStatic.getIsBassetOut(inputAddress, isCredit, outputAddress) + + const config = { + router, + input: inputAddress, + output: outputAddress, + amount: simpleToExactAmount(input === "musd" ? 100 : 10, 18), + isCredit, + } + + // Get estimated output via getUnwrapOutput + const amountOut = await unwrapper.getUnwrapOutput( + isBassetOut, + config.router, + config.input, + config.isCredit, + config.output, + config.amount, + ) + expect(amountOut.toString().length).to.be.gte(input === "musd" ? 18 : 9) + const minAmountOut = amountOut.mul(98).div(1e2) + + const outContract = IERC20__factory.connect(config.output, holder) + const tokenBalanceBefore = await outContract.balanceOf(holderAddress) + + // withdraw and unwrap + const saveVault = + input === "musd" + ? BoostedSavingsVaultImusdMainnet2__factory.connect(vaultAddress, holder) + : BoostedSavingsVaultImbtcMainnet2__factory.connect(vaultAddress, holder) + await saveVault.withdrawAndUnwrap(config.amount, minAmountOut, config.output, holderAddress, config.router, isBassetOut) + + const tokenBalanceAfter = await outContract.balanceOf(holderAddress) + const tokenBalanceDifference = tokenBalanceAfter.sub(tokenBalanceBefore) + assertBNClosePercent(tokenBalanceDifference, amountOut, 0.001) + expect(tokenBalanceAfter, "Token balance has increased").to.be.gt(tokenBalanceBefore) + } + + it("imUSD Vault redeem to bAsset", async () => { + await withdrawAndUnwrap(vmusdHolderAddress, musdAddress, "musd", daiAddress) + }) + + it("imUSD Vault redeem to fAsset", async () => { + await withdrawAndUnwrap(vmusdHolderAddress, alusdFeederPool, "musd", alusdAddress) + }) + it("imBTC Vault redeem to bAsset", async () => { + await withdrawAndUnwrap(vmbtcHolderAddress, mbtcAddress, "mbtc", wbtcAddress) + }) + + it("imBTC Vault redeem to fAsset", async () => { + await withdrawAndUnwrap(vhbtcmbtcHolderAddress, hbtcFeederPool, "mbtc", hbtcAddress) + }) + + it("Emits referrer successfully", async () => { + const saveContractProxy = SavingsContractImusdMainnet22__factory.connect(imusdAddress, musdHolder) + const musdContractProxy = ERC20__factory.connect(musdAddress, musdHolder) + await musdContractProxy.approve(imusdAddress, simpleToExactAmount(100, 18)) + const tx = await saveContractProxy["depositSavings(uint256,address,address)"]( + simpleToExactAmount(1, 18), + musdHolderAddress, + DEAD_ADDRESS, + ) + await expect(tx) + .to.emit(saveContractProxy, "Referral") + .withArgs(DEAD_ADDRESS, "0x8474DdbE98F5aA3179B3B3F5942D724aFcdec9f6", simpleToExactAmount(1, 18)) + }) + }) + }) +}) diff --git a/test-fork/savings/sc22-polygon-upgrade.spec.ts b/test-fork/savings/sc22-polygon-upgrade.spec.ts new file mode 100644 index 00000000..c304cf19 --- /dev/null +++ b/test-fork/savings/sc22-polygon-upgrade.spec.ts @@ -0,0 +1,427 @@ +import { impersonate } from "@utils/fork" +import { Signer, ContractFactory } from "ethers" +import { expect } from "chai" +import { network } from "hardhat" +import { deployContract } from "tasks/utils/deploy-utils" +// Polygon imUSD Vault +import { StakingRewardsWithPlatformTokenImusdPolygon2__factory } from "types/generated/factories/StakingRewardsWithPlatformTokenImusdPolygon2__factory" +import { StakingRewardsWithPlatformTokenImusdPolygon2 } from "types/generated/StakingRewardsWithPlatformTokenImusdPolygon2" + +// Polygon imUSD Contract +import { SavingsContractImusdPolygon22__factory } from "types/generated/factories/SavingsContractImusdPolygon22__factory" +import { SavingsContractImusdPolygon22 } from "types/generated/SavingsContractImusdPolygon22" + +import { + DelayedProxyAdmin, + DelayedProxyAdmin__factory, + ERC20__factory, + IERC20__factory, + Unwrapper, + Unwrapper__factory, + AssetProxy__factory, +} from "types/generated" + +import { assertBNClosePercent, Chain, DEAD_ADDRESS, simpleToExactAmount } from "index" +import { BigNumber } from "@ethersproject/bignumber" +import { getChainAddress, resolveAddress } from "tasks/utils/networkAddressFactory" +import { upgradeContract } from "@utils/deploy" + +const chain = Chain.polygon +const delayedProxyAdminAddress = getChainAddress("DelayedProxyAdmin", chain) +const multiSigAddress = "0x4aA2Dd5D5387E4b8dcf9b6Bfa4D9236038c3AD43" // 4/8 Multisig +const governorAddress = resolveAddress("Governor", chain) +const nexusAddress = getChainAddress("Nexus", chain) +const deployerAddress = getChainAddress("OperationsSigner", chain) +const imusdHolderAddress = "0x9d8B7A637859668A903797D9f02DE2Aa05e5b0a0" +const musdHolderAddress = "0xb14fFDB81E804D2792B6043B90aE5Ac973EcD53D" +const vmusdHolderAddress = "0x9d8B7A637859668A903797D9f02DE2Aa05e5b0a0" + +const daiAddress = resolveAddress("DAI", chain) +const fraxAddress = resolveAddress("FRAX", chain) +const musdAddress = resolveAddress("mUSD", chain) +const imusdAddress = resolveAddress("mUSD", chain, "savings") +const imusdVaultAddress = resolveAddress("mUSD", chain, "vault") +const fraxFeederPool = resolveAddress("FRAX", chain, "feederPool") +const mtaAddress = resolveAddress("MTA", chain) +const wmaticAddress = resolveAddress("WMATIC", chain) + +// DEPLOYMENT PIPELINE +// 1. Deploy Unwrapper +// 1.1. Set the Unwrapper address as constant in imUSD Vault via initialize +// 2. Upgrade and check storage +// 2.1. Vaults +// 2.2. SavingsContracts +// 3. Do some unwrapping +// 3.1. Directly to unwrapper +// 3.2. Via SavingsContracts +// 3.3. Via SavingsVaults +context("Unwrapper and Vault upgrades", () => { + let deployer: Signer + let musdHolder: Signer + let unwrapper: Unwrapper + let governor: Signer + let multiSig: Signer + let delayedProxyAdmin: DelayedProxyAdmin + + const redeemAndUnwrap = async ( + holderAddress: string, + router: string, + input: "musd" | "mbtc", + outputAddress: string, + isCredit = false, + ) => { + if (input === "mbtc") throw new Error("mbtc not supported") + + const holder = await impersonate(holderAddress) + const saveAddress = imusdAddress + let inputAddress = musdAddress + + if (input === "musd" && isCredit) { + inputAddress = imusdAddress + } else if (input === "musd" && !isCredit) { + inputAddress = musdAddress + } + + const amount = input === "musd" ? simpleToExactAmount(1, 14) : simpleToExactAmount(1, 14) + + const config = { + router, + input: inputAddress, + output: outputAddress, + amount: isCredit ? amount : amount.mul(10), + isCredit, + } + + // Get estimated output via getUnwrapOutput + const isBassetOut = await unwrapper.callStatic.getIsBassetOut(config.input, config.isCredit, config.output) + const amountOut = await unwrapper.getUnwrapOutput( + isBassetOut, + config.router, + config.input, + config.isCredit, + config.output, + config.amount, + ) + expect(amountOut.toString().length).to.be.gte(input === "musd" ? 14 : 4) + const minAmountOut = amountOut.mul(98).div(1e2) + const outContract = IERC20__factory.connect(config.output, holder) + const tokenBalanceBefore = await outContract.balanceOf(holderAddress) + const saveContract = SavingsContractImusdPolygon22__factory.connect(saveAddress, holder) + + const holderVaultBalanceBefore = await saveContract.balanceOf(holderAddress) + + await saveContract.redeemAndUnwrap( + config.amount, + config.isCredit, + minAmountOut, + config.output, + holderAddress, + config.router, + isBassetOut, + ) + + const tokenBalanceAfter = await outContract.balanceOf(holderAddress) + const holderVaultBalanceAfter = await saveContract.balanceOf(holderAddress) + + const tokenBalanceDifference = tokenBalanceAfter.sub(tokenBalanceBefore) + assertBNClosePercent(tokenBalanceDifference, amountOut, 0.001) + expect(tokenBalanceAfter, "Token balance has increased").to.be.gt(tokenBalanceBefore) + expect(holderVaultBalanceAfter, "Vault balance has decreased").to.be.lt(holderVaultBalanceBefore) + } + /** + * imUSD Vault on polygon was deployed with the wrong proxy admin, this fix the issue setting the DelayedProxyAdmin as it's proxy admin + * It changes from multiSig to delayedProxyAdmin.address + */ + async function fixImusdVaultProxyAdmin() { + const imusdVaultAssetProxy = AssetProxy__factory.connect(imusdVaultAddress, multiSig) + await imusdVaultAssetProxy.changeAdmin(delayedProxyAdmin.address) + } + before("reset block number", async () => { + await network.provider.request({ + method: "hardhat_reset", + params: [ + { + forking: { + jsonRpcUrl: process.env.NODE_URL, + blockNumber: 24186168, + }, + }, + ], + }) + + musdHolder = await impersonate(musdHolderAddress) + deployer = await impersonate(deployerAddress) + governor = await impersonate(governorAddress) + multiSig = await impersonate(multiSigAddress) + delayedProxyAdmin = DelayedProxyAdmin__factory.connect(delayedProxyAdminAddress, governor) + }) + it("Test connectivity", async () => { + const startEther = await deployer.getBalance() + const address = await deployer.getTransactionCount() + console.log(`Deployer ${address} has ${startEther} Ether`) + }) + + context("Stage 1", () => { + it("Deploys the unwrapper proxy contract ", async () => { + unwrapper = await deployContract(new Unwrapper__factory(deployer), "Unwrapper", [nexusAddress]) + expect(unwrapper.address).to.length(42) + // approve tokens for router + const routers = [fraxFeederPool] + const tokens = [musdAddress] + await unwrapper.connect(governor).approve(routers, tokens) + }) + }) + + context("Stage 2", () => { + describe.skip("2.1 Upgrading vaults", () => { + it("Upgrades the imUSD Vault", async () => { + await fixImusdVaultProxyAdmin() + + const constructorArguments = [ + nexusAddress, // 0x3c6fbb8cbfcb75ecec5128e9f73307f2cb33f2f6 deployed + imusdAddress, // imUSD + mtaAddress, // MTA bridged to Polygon + wmaticAddress, // Wrapped Matic on Polygon + ] + + const saveVaultImpl = await deployContract( + new StakingRewardsWithPlatformTokenImusdPolygon2__factory(deployer), + "mStable: mUSD Savings Vault", + constructorArguments, + ) + await upgradeContract( + StakingRewardsWithPlatformTokenImusdPolygon2__factory as unknown as ContractFactory, + saveVaultImpl, + imusdVaultAddress, + governor, + delayedProxyAdmin, + ) + expect(await delayedProxyAdmin.getProxyImplementation(imusdVaultAddress)).eq(saveVaultImpl.address) + }) + }) + describe("2.2 Upgrading savings contracts", () => { + it("Upgrades the imUSD contract", async () => { + const constructorArguments = [nexusAddress, musdAddress, unwrapper.address] + const musdSaveImpl = await deployContract( + new SavingsContractImusdPolygon22__factory(deployer), + "mStable: mUSD Savings Contract", + constructorArguments, + ) + const saveContractProxy = await upgradeContract( + SavingsContractImusdPolygon22__factory as unknown as ContractFactory, + musdSaveImpl, + imusdAddress, + governor, + delayedProxyAdmin, + ) + + const unwrapperAddress = await saveContractProxy.unwrapper() + expect(unwrapperAddress).to.eq(unwrapper.address) + expect(await delayedProxyAdmin.getProxyImplementation(imusdAddress)).eq(musdSaveImpl.address) + expect(musdAddress).eq(await musdSaveImpl.underlying()) + }) + + it("imUSD contract works after upgraded", async () => { + await redeemAndUnwrap(imusdHolderAddress, musdAddress, "musd", daiAddress) + }) + }) + }) + + context("Stage 3", () => { + describe("3.1 Directly", () => { + it("Can call getIsBassetOut & it functions correctly", async () => { + const isCredit = true + expect(await unwrapper.callStatic.getIsBassetOut(musdAddress, !isCredit, daiAddress)).to.eq(true) + expect(await unwrapper.callStatic.getIsBassetOut(musdAddress, !isCredit, musdAddress)).to.eq(false) + expect(await unwrapper.callStatic.getIsBassetOut(musdAddress, !isCredit, fraxAddress)).to.eq(false) + }) + + const validateAssetRedemption = async ( + config: { + router: string + input: string + output: string + amount: BigNumber + isCredit: boolean + }, + signer: Signer, + ) => { + // Get estimated output via getUnwrapOutput + const signerAddress = await signer.getAddress() + const isBassetOut = await unwrapper.callStatic.getIsBassetOut(config.input, false, config.output) + + const amountOut = await unwrapper.getUnwrapOutput( + isBassetOut, + config.router, + config.input, + config.isCredit, + config.output, + config.amount, + ) + expect(amountOut.toString().length).to.be.gte(18) + const minAmountOut = amountOut.mul(98).div(1e2) + + const newConfig = { + ...config, + minAmountOut, + beneficiary: signerAddress, + } + + // check balance before + const tokenOut = IERC20__factory.connect(config.output, signer) + const tokenBalanceBefore = await tokenOut.balanceOf(signerAddress) + + // approve musd for unwrapping + const tokenInput = IERC20__factory.connect(config.input, signer) + await tokenInput.approve(unwrapper.address, config.amount) + + // redeem to basset via unwrapAndSend + await unwrapper + .connect(signer) + .unwrapAndSend( + isBassetOut, + newConfig.router, + newConfig.input, + newConfig.output, + newConfig.amount, + newConfig.minAmountOut, + newConfig.beneficiary, + ) + + // check balance after + const tokenBalanceAfter = await tokenOut.balanceOf(signerAddress) + expect(tokenBalanceAfter, "Token balance has increased").to.be.gt(tokenBalanceBefore) + } + + it("Receives the correct output from getUnwrapOutput", async () => { + const config = { + router: musdAddress, + input: musdAddress, + output: daiAddress, + amount: simpleToExactAmount(1, 18), + isCredit: false, + } + const isBassetOut = await unwrapper.callStatic.getIsBassetOut(config.input, config.isCredit, config.output) + const output = await unwrapper.getUnwrapOutput( + isBassetOut, + config.router, + config.input, + config.isCredit, + config.output, + config.amount, + ) + expect(output.toString()).to.be.length(18) + }) + + it("mUSD redeem to bAsset via unwrapAndSend", async () => { + const config = { + router: musdAddress, + input: musdAddress, + output: daiAddress, + amount: simpleToExactAmount(1, 18), + isCredit: false, + } + + await validateAssetRedemption(config, musdHolder) + }) + + it("mUSD redeem to fAsset via unwrapAndSend", async () => { + const config = { + router: fraxFeederPool, + input: musdAddress, + output: fraxAddress, + amount: simpleToExactAmount(1, 18), + isCredit: false, + } + await validateAssetRedemption(config, musdHolder) + }) + }) + + describe("3.2 Via SavingsContracts", () => { + it("mUSD contract redeem to bAsset", async () => { + await redeemAndUnwrap(imusdHolderAddress, musdAddress, "musd", daiAddress) + }) + + it("mUSD contract redeem to fAsset", async () => { + await redeemAndUnwrap(imusdHolderAddress, fraxFeederPool, "musd", fraxAddress) + }) + // credits + it("imUSD contract redeem to bAsset", async () => { + await redeemAndUnwrap(imusdHolderAddress, musdAddress, "musd", daiAddress, true) + }) + + it("imUSD contract redeem to fAsset", async () => { + await redeemAndUnwrap(imusdHolderAddress, fraxFeederPool, "musd", fraxAddress, true) + }) + }) + + describe("3.3 Via Vaults", () => { + const withdrawAndUnwrap = async (holderAddress: string, router: string, input: "musd" | "mbtc", outputAddress: string) => { + if (input === "mbtc") throw new Error("mBTC not supported") + + const isCredit = true + const holder = await impersonate(holderAddress) + const vaultAddress = imusdVaultAddress + const inputAddress = imusdAddress + const isBassetOut = await unwrapper.callStatic.getIsBassetOut(inputAddress, isCredit, outputAddress) + const config = { + router, + input: inputAddress, + output: outputAddress, + amount: simpleToExactAmount(1, 18), + isCredit, + } + + // Get estimated output via getUnwrapOutput + const amountOut = await unwrapper.getUnwrapOutput( + isBassetOut, + config.router, + config.input, + config.isCredit, + config.output, + config.amount, + ) + expect(amountOut.toString().length).to.be.gte(input === "musd" ? 18 : 9) + const minAmountOut = amountOut.mul(98).div(1e2) + + const outContract = IERC20__factory.connect(config.output, holder) + const tokenBalanceBefore = await outContract.balanceOf(holderAddress) + + // withdraw and unwrap + const saveVault = StakingRewardsWithPlatformTokenImusdPolygon2__factory.connect(vaultAddress, holder) + const holderVaultBalanceBefore = await saveVault.balanceOf(holderAddress) + + await saveVault.withdrawAndUnwrap(config.amount, minAmountOut, config.output, holderAddress, config.router, isBassetOut) + const holderVaultBalanceAfter = await saveVault.balanceOf(holderAddress) + + const tokenBalanceAfter = await outContract.balanceOf(holderAddress) + const tokenBalanceDifference = tokenBalanceAfter.sub(tokenBalanceBefore) + assertBNClosePercent(tokenBalanceDifference, amountOut, 0.001) + expect(tokenBalanceAfter, "Token balance has increased").to.be.gt(tokenBalanceBefore) + expect(holderVaultBalanceAfter, "Vault balance has decreased").to.be.lt(holderVaultBalanceBefore) + } + + it("imUSD Vault redeem to bAsset", async () => { + await withdrawAndUnwrap(vmusdHolderAddress, musdAddress, "musd", daiAddress) + }) + + it("imUSD Vault redeem to fAsset", async () => { + await withdrawAndUnwrap(vmusdHolderAddress, fraxFeederPool, "musd", fraxAddress) + }) + + it("Emits referrer successfully", async () => { + const saveContractProxy = SavingsContractImusdPolygon22__factory.connect(imusdAddress, musdHolder) + const musdContractProxy = ERC20__factory.connect(musdAddress, musdHolder) + await musdContractProxy.approve(imusdAddress, simpleToExactAmount(100, 18)) + const tx = await saveContractProxy["depositSavings(uint256,address,address)"]( + simpleToExactAmount(1, 18), + musdHolderAddress, + DEAD_ADDRESS, + ) + await expect(tx) + .to.emit(saveContractProxy, "Referral") + .withArgs(DEAD_ADDRESS, musdHolderAddress, simpleToExactAmount(1, 18)) + }) + }) + }) +}) diff --git a/test/savings/savings-contract.spec.ts b/test/savings/savings-contract.spec.ts index 581de8d3..150bb239 100644 --- a/test/savings/savings-contract.spec.ts +++ b/test/savings/savings-contract.spec.ts @@ -10,6 +10,7 @@ import { Masset, AssetProxy__factory, ExposedMasset, + IERC4626Vault, MockConnector__factory, MockERC20, MockERC20__factory, @@ -32,7 +33,7 @@ import { } from "types/generated" import { getTimestamp } from "@utils/time" import { IModuleBehaviourContext, shouldBehaveLikeModule } from "../shared/Module.behaviour" -// import { IERC4626BehaviourContext, shouldBehaveLikeERC4626 } from "../shared/ERC4626.behaviour" +import { IERC4626BehaviourContext, shouldBehaveLikeERC4626 } from "../shared/ERC4626.behaviour" interface Balances { totalCredits: BN @@ -219,7 +220,7 @@ describe("SavingsContract", async () => { let bob: Account let charlie: Account const ctx: Partial = {} - // const ctxVault: Partial = {} + const ctxVault: Partial = {} const initialExchangeRate = simpleToExactAmount(1, 17) let mAssetMachine: MassetMachine @@ -301,15 +302,17 @@ describe("SavingsContract", async () => { }) shouldBehaveLikeModule(ctx as IModuleBehaviourContext) }) - // describe("behave like a Vault ERC4626", async () => { - // beforeEach(async () => { - // await createNewSavingsContract() - // ctxVault.vault = savingsContract - // ctxVault.token = masset - // ctx.sa = sa - // }) - // shouldBehaveLikeERC4626(ctx as IERC4626BehaviourContext) - // }) + describe("behave like a Vault ERC4626", async () => { + beforeEach(async () => { + await createNewSavingsContract() + await savingsContract.connect(sa.governor.signer).automateInterestCollectionFlag(false) + + ctxVault.vault = savingsContract as unknown as IERC4626Vault + ctxVault.asset = masset + ctxVault.sa = sa + }) + shouldBehaveLikeERC4626(ctxVault as IERC4626BehaviourContext) + }) }) describe("constructor", async () => { @@ -1510,7 +1513,6 @@ describe("SavingsContract", async () => { expect(dataEnd.balances.user).eq(data.balances.user.add(deposit)) }) }) - ;[ { deposit: depositSavingsFn, redeem: redeemCreditsFn }, { deposit: deposit4626Fn, redeem: redeem4626Fn }, diff --git a/test/shared/ERC4626.behaviour.ts b/test/shared/ERC4626.behaviour.ts new file mode 100644 index 00000000..cf521f51 --- /dev/null +++ b/test/shared/ERC4626.behaviour.ts @@ -0,0 +1,328 @@ +import { ZERO_ADDRESS } from "@utils/constants" +import { MassetDetails, MassetMachine, StandardAccounts } from "@utils/machines" +import { BN, simpleToExactAmount } from "@utils/math" +import { expect } from "chai" +import { Account } from "types" +import { ERC20, ERC205, IERC20Metadata, IERC4626Vault } from "types/generated" + +const safeInfinity = BN.from(2).pow(96).sub(1) + +export interface IERC4626BehaviourContext { + vault: IERC4626Vault + asset: ERC20 + sa: StandardAccounts + mAssetMachine: MassetMachine + details: MassetDetails +} + +export function shouldBehaveLikeERC4626(ctx: IERC4626BehaviourContext): void { + let assetsAmount: BN + let sharesAmount: BN + let alice: Account + let bob: Account + beforeEach("init", async () => { + assetsAmount = simpleToExactAmount(1, await (ctx.asset as unknown as IERC20Metadata).decimals()) + sharesAmount = simpleToExactAmount(10, await (ctx.asset as unknown as IERC20Metadata).decimals()) + alice = ctx.sa.default + bob = ctx.sa.dummy2 + }) + it("should properly store valid arguments", async () => { + expect(await ctx.vault.asset(), "asset").to.eq(ctx.asset.address) + }) + describe("deposit", async () => { + it("should deposit assets to the vault", async () => { + await ctx.asset.approve(ctx.vault.address, simpleToExactAmount(1, 21)) + const shares = await ctx.vault.previewDeposit(assetsAmount) + + expect(await ctx.vault.maxDeposit(alice.address), "max deposit").to.gte(assetsAmount) + expect(await ctx.vault.maxMint(alice.address), "max mint").to.gte(shares) + + expect(await ctx.vault.maxRedeem(alice.address), "max redeem").to.eq(0) + expect(await ctx.vault.maxWithdraw(alice.address), "max withdraw").to.eq(0) + expect(await ctx.vault.totalAssets(), "totalAssets").to.eq(0) + expect(await ctx.vault.convertToShares(assetsAmount), "convertToShares").to.lte(shares) + + // Test + const tx = await ctx.vault.connect(alice.signer)["deposit(uint256,address)"](assetsAmount, alice.address) + // Verify events, storage change, balance, etc. + await expect(tx).to.emit(ctx.vault, "Deposit").withArgs(alice.address, alice.address, assetsAmount, shares) + expect(await ctx.vault.maxRedeem(alice.address), "max redeem").to.lte(shares) + expect(await ctx.vault.maxWithdraw(alice.address), "max withdraw").to.lte(assetsAmount) + expect(await ctx.vault.totalAssets(), "totalAssets").to.eq(assetsAmount) + }) + it("fails if deposits zero", async () => { + await expect(ctx.vault.connect(ctx.sa.default.signer)["deposit(uint256,address)"](0, alice.address)).to.be.revertedWith( + "Must deposit something", + ) + }) + it("fails if receiver is zero", async () => { + await expect(ctx.vault.connect(ctx.sa.default.signer)["deposit(uint256,address)"](10, ZERO_ADDRESS)).to.be.revertedWith( + "Invalid beneficiary address", + ) + }) + }) + + describe("mint", async () => { + it("should mint shares to the vault", async () => { + await ctx.asset.approve(ctx.vault.address, simpleToExactAmount(1, 21)) + // const shares = sharesAmount + const assets = await ctx.vault.previewMint(sharesAmount) + const shares = await ctx.vault.previewDeposit(assetsAmount) + + expect(await ctx.vault.maxDeposit(alice.address), "max deposit").to.gte(assets) + expect(await ctx.vault.maxMint(alice.address), "max mint").to.gte(shares) + + expect(await ctx.vault.maxRedeem(alice.address), "max redeem").to.eq(0) + expect(await ctx.vault.maxWithdraw(alice.address), "max withdraw").to.eq(0) + expect(await ctx.vault.totalAssets(), "totalAssets").to.eq(0) + + expect(await ctx.vault.convertToShares(assets), "convertToShares").to.lte(shares) + expect(await ctx.vault.convertToAssets(shares), "convertToShares").to.lte(assets) + + const tx = await ctx.vault.connect(alice.signer).mint(shares, alice.address) + // Verify events, storage change, balance, etc. + await expect(tx).to.emit(ctx.vault, "Deposit").withArgs(alice.address, alice.address, assets, shares) + + expect(await ctx.vault.maxRedeem(alice.address), "max redeem").to.lte(shares) + expect(await ctx.vault.maxWithdraw(alice.address), "max withdraw").to.lte(assets) + expect(await ctx.vault.totalAssets(), "totalAssets").to.eq(assets) + }) + it("fails if mint zero", async () => { + await expect(ctx.vault.connect(ctx.sa.default.signer)["mint(uint256,address)"](0, alice.address)).to.be.revertedWith( + "Must deposit something", + ) + }) + it("fails if receiver is zero", async () => { + await expect(ctx.vault.connect(ctx.sa.default.signer)["mint(uint256,address)"](10, ZERO_ADDRESS)).to.be.revertedWith( + "Invalid beneficiary address", + ) + }) + }) + + describe("withdraw", async () => { + it("from the vault, same caller, receiver and owner", async () => { + await ctx.asset.approve(ctx.vault.address, simpleToExactAmount(1, 21)) + + expect(await ctx.vault.maxWithdraw(alice.address), "max withdraw").to.eq(0) + expect(await ctx.vault.totalAssets(), "totalAssets").to.eq(0) + + await ctx.vault.connect(alice.signer)["deposit(uint256,address)"](assetsAmount, alice.address) + expect(await ctx.vault.maxWithdraw(alice.address), "max withdraw").to.gt(0) + expect(await ctx.vault.totalAssets(), "totalAssets").to.gt(0) + const shares = await ctx.vault.previewWithdraw(assetsAmount) + expect(await ctx.vault.maxRedeem(alice.address), "max redeem").to.eq(shares) + + // Test + const tx = await ctx.vault.connect(alice.signer).withdraw(assetsAmount, alice.address, alice.address) + // Verify events, storage change, balance, etc. + await expect(tx).to.emit(ctx.vault, "Withdraw").withArgs(alice.address, alice.address, alice.address, assetsAmount, shares) + expect(await ctx.vault.maxRedeem(alice.address), "max redeem").to.eq(0) + expect(await ctx.vault.maxWithdraw(alice.address), "max withdraw").to.eq(0) + expect(await ctx.vault.totalAssets(), "totalAssets").to.eq(0) + }) + it("from the vault, caller != receiver and caller = owner", async () => { + // Alice deposits assets (owner), Alice withdraws assets (caller), Bob receives assets (receiver) + await ctx.asset.connect(alice.signer).approve(ctx.vault.address, simpleToExactAmount(1, 21)) + + expect(await ctx.vault.maxWithdraw(alice.address), "max withdraw").to.eq(0) + expect(await ctx.vault.totalAssets(), "totalAssets").to.eq(0) + + await ctx.vault.connect(alice.signer)["deposit(uint256,address)"](assetsAmount, alice.address) + expect(await ctx.vault.maxWithdraw(alice.address), "max withdraw").to.gt(0) + expect(await ctx.vault.totalAssets(), "totalAssets").to.gt(0) + const shares = await ctx.vault.previewWithdraw(assetsAmount) + expect(await ctx.vault.maxRedeem(alice.address), "max redeem").to.eq(shares) + + // Test + const tx = await ctx.vault.connect(alice.signer).withdraw(assetsAmount, bob.address, alice.address) + // Verify events, storage change, balance, etc. + await expect(tx).to.emit(ctx.vault, "Withdraw").withArgs(alice.address, bob.address, alice.address, assetsAmount, shares) + expect(await ctx.vault.maxRedeem(alice.address), "max redeem").to.eq(0) + expect(await ctx.vault.maxWithdraw(alice.address), "max withdraw").to.eq(0) + expect(await ctx.vault.totalAssets(), "totalAssets").to.eq(0) + }) + it("from the vault caller != owner, infinite approval", async () => { + // Alice deposits assets (owner), Bob withdraws assets (caller), Bob receives assets (receiver) + await ctx.asset.connect(alice.signer).approve(ctx.vault.address, simpleToExactAmount(1, 21)) + await (ctx.vault.connect(alice.signer) as unknown as ERC205).approve(bob.address, safeInfinity) + + expect(await ctx.vault.maxWithdraw(alice.address), "max withdraw").to.eq(0) + expect(await ctx.vault.totalAssets(), "totalAssets").to.eq(0) + + await ctx.vault.connect(alice.signer)["deposit(uint256,address)"](assetsAmount, alice.address) + expect(await ctx.vault.maxWithdraw(alice.address), "max withdraw").to.gt(0) + expect(await ctx.vault.totalAssets(), "totalAssets").to.gt(0) + const shares = await ctx.vault.previewWithdraw(assetsAmount) + expect(await ctx.vault.maxRedeem(alice.address), "max redeem").to.eq(shares) + + // Test + const tx = await ctx.vault.connect(bob.signer).withdraw(assetsAmount, bob.address, alice.address) + // Verify events, storage change, balance, etc. + await expect(tx).to.emit(ctx.vault, "Withdraw").withArgs(bob.address, bob.address, alice.address, assetsAmount, shares) + expect(await ctx.vault.maxRedeem(alice.address), "max redeem").to.eq(0) + expect(await ctx.vault.maxWithdraw(alice.address), "max withdraw").to.eq(0) + expect(await ctx.vault.totalAssets(), "totalAssets").to.eq(0) + }) + it("from the vault, caller != receiver and caller != owner", async () => { + // Alice deposits assets (owner), Bob withdraws assets (caller), Bob receives assets (receiver) + await ctx.asset.connect(alice.signer).approve(ctx.vault.address, simpleToExactAmount(1, 21)) + await (ctx.vault.connect(alice.signer) as unknown as ERC205).approve(bob.address, simpleToExactAmount(1, 21)) + + expect(await ctx.vault.maxWithdraw(alice.address), "max withdraw").to.eq(0) + expect(await ctx.vault.totalAssets(), "totalAssets").to.eq(0) + + await ctx.vault.connect(alice.signer)["deposit(uint256,address)"](assetsAmount, alice.address) + expect(await ctx.vault.maxWithdraw(alice.address), "max withdraw").to.gt(0) + expect(await ctx.vault.totalAssets(), "totalAssets").to.gt(0) + const shares = await ctx.vault.previewWithdraw(assetsAmount) + expect(await ctx.vault.maxRedeem(alice.address), "max redeem").to.eq(shares) + + // Test + const tx = await ctx.vault.connect(bob.signer).withdraw(assetsAmount, bob.address, alice.address) + // Verify events, storage change, balance, etc. + await expect(tx).to.emit(ctx.vault, "Withdraw").withArgs(bob.address, bob.address, alice.address, assetsAmount, shares) + expect(await ctx.vault.maxRedeem(alice.address), "max redeem").to.eq(0) + expect(await ctx.vault.maxWithdraw(alice.address), "max withdraw").to.eq(0) + expect(await ctx.vault.totalAssets(), "totalAssets").to.eq(0) + }) + it("fails if deposits zero", async () => { + await expect(ctx.vault.connect(ctx.sa.default.signer).withdraw(0, alice.address, alice.address)).to.be.revertedWith( + "Must withdraw something", + ) + }) + it("fails if receiver is zero", async () => { + await expect(ctx.vault.connect(ctx.sa.default.signer).withdraw(10, ZERO_ADDRESS, ZERO_ADDRESS)).to.be.revertedWith( + "Invalid beneficiary address", + ) + }) + it("fail if caller != owner and it has not allowance", async () => { + // Alice deposits assets (owner), Bob withdraws assets (caller), Bob receives assets (receiver) + await ctx.asset.connect(alice.signer).approve(ctx.vault.address, simpleToExactAmount(1, 21)) + + expect(await ctx.vault.maxWithdraw(alice.address), "max withdraw").to.eq(0) + expect(await ctx.vault.totalAssets(), "totalAssets").to.eq(0) + + await ctx.vault.connect(alice.signer)["deposit(uint256,address)"](assetsAmount, alice.address) + expect(await ctx.vault.maxWithdraw(alice.address), "max withdraw").to.gt(0) + expect(await ctx.vault.totalAssets(), "totalAssets").to.gt(0) + const shares = await ctx.vault.previewWithdraw(assetsAmount) + expect(await ctx.vault.maxRedeem(alice.address), "max redeem").to.eq(shares) + + // Test + const tx = ctx.vault.connect(bob.signer).withdraw(assetsAmount, bob.address, alice.address) + // Verify events, storage change, balance, etc. + await expect(tx).to.be.revertedWith("Amount exceeds allowance") + }) + }) + describe("redeem", async () => { + it("from the vault, same caller, receiver and owner", async () => { + await ctx.asset.approve(ctx.vault.address, simpleToExactAmount(1, 21)) + + const assets = await ctx.vault.previewRedeem(sharesAmount) + expect(await ctx.vault.maxRedeem(alice.address), "max maxRedeem").to.eq(0) + expect(await ctx.vault.totalAssets(), "totalAssets").to.eq(0) + + await ctx.vault.connect(alice.signer)["deposit(uint256,address)"](assets, alice.address) + expect(await ctx.vault.maxRedeem(alice.address), "max maxRedeem").to.gt(0) + expect(await ctx.vault.totalAssets(), "totalAssets").to.gt(0) + const shares = await ctx.vault.maxRedeem(alice.address) + + // Test + const tx = await ctx.vault.connect(alice.signer)["redeem(uint256,address,address)"](shares, alice.address, alice.address) + // Verify events, storage change, balance, etc. + await expect(tx).to.emit(ctx.vault, "Withdraw").withArgs(alice.address, alice.address, alice.address, assets, shares) + expect(await ctx.vault.maxRedeem(alice.address), "max redeem").to.eq(0) + expect(await ctx.vault.maxWithdraw(alice.address), "max withdraw").to.eq(0) + expect(await ctx.vault.totalAssets(), "totalAssets").to.eq(0) + }) + it("from the vault, caller != receiver and caller = owner", async () => { + // Alice deposits assets (owner), Alice withdraws assets (caller), Bob receives assets (receiver) + await ctx.asset.connect(alice.signer).approve(ctx.vault.address, simpleToExactAmount(1, 21)) + const assets = await ctx.vault.previewRedeem(sharesAmount) + + expect(await ctx.vault.maxRedeem(alice.address), "max withdraw").to.eq(0) + expect(await ctx.vault.totalAssets(), "totalAssets").to.eq(0) + + await ctx.vault.connect(alice.signer)["deposit(uint256,address)"](assetsAmount, alice.address) + expect(await ctx.vault.maxWithdraw(alice.address), "max withdraw").to.eq(assets) + expect(await ctx.vault.totalAssets(), "totalAssets").to.gt(0) + const shares = await ctx.vault.maxRedeem(alice.address) + + // Test + const tx = await ctx.vault.connect(alice.signer)["redeem(uint256,address,address)"](shares, bob.address, alice.address) + // Verify events, storage change, balance, etc. + await expect(tx).to.emit(ctx.vault, "Withdraw").withArgs(alice.address, bob.address, alice.address, assets, shares) + expect(await ctx.vault.maxRedeem(alice.address), "max redeem").to.eq(0) + expect(await ctx.vault.maxWithdraw(alice.address), "max withdraw").to.eq(0) + expect(await ctx.vault.totalAssets(), "totalAssets").to.eq(0) + }) + it("from the vault caller != owner, infinite approval", async () => { + // Alice deposits assets (owner), Bob withdraws assets (caller), Bob receives assets (receiver) + await ctx.asset.connect(alice.signer).approve(ctx.vault.address, simpleToExactAmount(1, 21)) + await (ctx.vault.connect(alice.signer) as unknown as ERC205).approve(bob.address, safeInfinity) + const assets = await ctx.vault.previewRedeem(sharesAmount) + + expect(await ctx.vault.maxWithdraw(alice.address), "max withdraw").to.eq(0) + expect(await ctx.vault.totalAssets(), "totalAssets").to.eq(0) + + await ctx.vault.connect(alice.signer)["deposit(uint256,address)"](assets, alice.address) + expect(await ctx.vault.maxWithdraw(alice.address), "max withdraw").to.gt(0) + expect(await ctx.vault.totalAssets(), "totalAssets").to.gt(0) + const shares = await ctx.vault.maxRedeem(alice.address) + + // Test + const tx = await ctx.vault.connect(bob.signer)["redeem(uint256,address,address)"](shares, bob.address, alice.address) + // Verify events, storage change, balance, etc. + await expect(tx).to.emit(ctx.vault, "Withdraw").withArgs(bob.address, bob.address, alice.address, assets, shares) + expect(await ctx.vault.maxRedeem(alice.address), "max redeem").to.eq(0) + expect(await ctx.vault.maxWithdraw(alice.address), "max withdraw").to.eq(0) + expect(await ctx.vault.totalAssets(), "totalAssets").to.eq(0) + }) + it("from the vault, caller != receiver and caller != owner", async () => { + // Alice deposits assets (owner), Bob withdraws assets (caller), Bob receives assets (receiver) + await ctx.asset.connect(alice.signer).approve(ctx.vault.address, simpleToExactAmount(1, 21)) + await (ctx.vault.connect(alice.signer) as unknown as ERC205).approve(bob.address, simpleToExactAmount(1, 21)) + + const assets = await ctx.vault.previewRedeem(sharesAmount) + expect(await ctx.vault.maxWithdraw(alice.address), "max withdraw").to.eq(0) + expect(await ctx.vault.totalAssets(), "totalAssets").to.eq(0) + + await ctx.vault.connect(alice.signer)["deposit(uint256,address)"](assets, alice.address) + expect(await ctx.vault.maxWithdraw(alice.address), "max withdraw").to.gt(0) + expect(await ctx.vault.totalAssets(), "totalAssets").to.gt(0) + const shares = await ctx.vault.maxRedeem(alice.address) + + // Test + const tx = await ctx.vault.connect(bob.signer)["redeem(uint256,address,address)"](shares, bob.address, alice.address) + // Verify events, storage change, balance, etc. + await expect(tx).to.emit(ctx.vault, "Withdraw").withArgs(bob.address, bob.address, alice.address, assets, shares) + expect(await ctx.vault.maxRedeem(alice.address), "max redeem").to.eq(0) + expect(await ctx.vault.maxWithdraw(alice.address), "max withdraw").to.eq(0) + expect(await ctx.vault.totalAssets(), "totalAssets").to.eq(0) + }) + it("fails if deposits zero", async () => { + await expect( + ctx.vault.connect(ctx.sa.default.signer)["redeem(uint256,address,address)"](0, alice.address, alice.address), + ).to.be.revertedWith("Must withdraw something") + }) + it("fails if receiver is zero", async () => { + await expect( + ctx.vault.connect(ctx.sa.default.signer)["redeem(uint256,address,address)"](10, ZERO_ADDRESS, ZERO_ADDRESS), + ).to.be.revertedWith("Invalid beneficiary address") + }) + it("fail if caller != owner and it has not allowance", async () => { + // Alice deposits assets (owner), Bob withdraws assets (caller), Bob receives assets (receiver) + await ctx.asset.connect(alice.signer).approve(ctx.vault.address, simpleToExactAmount(1, 21)) + const assets = await ctx.vault.previewRedeem(sharesAmount) + + await ctx.vault.connect(alice.signer)["deposit(uint256,address)"](assets, alice.address) + + // Test + const tx = ctx.vault.connect(bob.signer)["redeem(uint256,address,address)"](sharesAmount, bob.address, alice.address) + // Verify events, storage change, balance, etc. + await expect(tx).to.be.revertedWith("Amount exceeds allowance") + }) + }) +} + +export default shouldBehaveLikeERC4626 diff --git a/test/shared/ERC4626.behaviours.ts b/test/shared/ERC4626.behaviours.ts deleted file mode 100644 index c5b1bd1d..00000000 --- a/test/shared/ERC4626.behaviours.ts +++ /dev/null @@ -1,173 +0,0 @@ - -import hre, { ethers } from "hardhat"; -import { expect } from "chai" -import { Signer } from "ethers" -import { simpleToExactAmount, BN } from "@utils/math" -import { IERC4626Vault, IERC4626Vault__factory, AbstractVault, AbstractVault__factory, MockNexus, ERC20 } from "types/generated" -import { Account } from "types" -import { MassetMachine, MassetDetails, StandardAccounts } from "@utils/machines" -import { ZERO_ADDRESS } from "@utils/constants" - - - -export interface IERC4626BehaviourContext { - vault: IERC4626Vault - asset: ERC20 - sa: StandardAccounts - mAssetMachine: MassetMachine - owner: Account - receiver: Account - anotherAccount: Account - details: MassetDetails -} - -export async function shouldBehaveLikeERC4626(ctx: IERC4626BehaviourContext, errorPrefix: string, initialSupply: BN): Promise { - - const assetsAmount = simpleToExactAmount(1, await ctx.asset.decimals()) - const sharesAmount = simpleToExactAmount(10, await ctx.asset.decimals()) - describe('ERC4626', () => { - - before("init contract", async () => { - }) - beforeEach(async () => { /* before each context */ }) - - describe("constructor", async () => { - it("should properly store valid arguments", async () => { - expect(await ctx.vault.asset(), "asset").to.eq(ctx.asset.address); - }) - }) - - // - describe("deposit", async () => { - beforeEach(async () => { /* before each context */ }) - - it('deposit should ...', async () => { - await ctx.asset.approve(ctx.vault.address, simpleToExactAmount(1, 21)) - - const tx = await ctx.vault.connect(ctx.owner.signer).deposit(assetsAmount, ctx.receiver.address) - // Verify events, storage change, balance, etc. - // await expect(tx).to.emit(abstractVault, "EVENT-NAME").withArgs("ARGUMENT 1", "ARGUMENT 2"); - - }); - it('fails if ...', async () => { - await expect(ctx.vault.connect(ctx.owner.signer).deposit(assetsAmount, ctx.receiver.address), "fails due to ").to.be.revertedWith("EXPECTED ERROR"); - }); - }); - - - // - describe("mint", async () => { - beforeEach(async () => { /* before each context */ }) - - it('mint should ...', async () => { - const tx = await ctx.vault.connect(ctx.owner.signer).mint(sharesAmount, ctx.receiver.address) - // Verify events, storage change, balance, etc. - // await expect(tx).to.emit(abstractVault, "EVENT-NAME").withArgs("ARGUMENT 1", "ARGUMENT 2"); - - }); - it('fails if ...', async () => { - await expect(ctx.vault.connect(ctx.owner.signer).mint(sharesAmount, ctx.receiver.address), "fails due to ").to.be.revertedWith("EXPECTED ERROR"); - }); - }); - - - // - describe("withdraw", async () => { - beforeEach(async () => { /* before each context */ }) - - it('withdraw should ...', async () => { - const tx = await ctx.vault.connect(ctx.owner.signer).withdraw(assetsAmount, ctx.receiver.address, ctx.owner.address) - // Verify events, storage change, balance, etc. - // await expect(tx).to.emit(abstractVault, "EVENT-NAME").withArgs("ARGUMENT 1", "ARGUMENT 2"); - - }); - it('fails if ...', async () => { - await expect(ctx.vault.connect(ctx.owner.signer).withdraw(assetsAmount, ctx.receiver.address, ctx.owner.address), "fails due to ").to.be.revertedWith("EXPECTED ERROR"); - }); - }); - - - // - describe("redeem", async () => { - beforeEach(async () => { /* before each context */ }) - - it('redeem should ...', async () => { - const tx = await ctx.vault.connect(ctx.owner.signer).redeem(sharesAmount, ctx.receiver.address, ctx.owner.address) - // Verify events, storage change, balance, etc. - // await expect(tx).to.emit(abstractVault, "EVENT-NAME").withArgs("ARGUMENT 1", "ARGUMENT 2"); - - }); - it('fails if ...', async () => { - await expect(ctx.vault.connect(ctx.owner.signer).redeem(sharesAmount, ctx.receiver.address, ctx.owner.address), "fails due to ").to.be.revertedWith("EXPECTED ERROR"); - }); - }); - - - describe("read only functions", async () => { - beforeEach(async () => { /* before each context */ }) - - it('previewDeposit should ...', async () => { - const response = await ctx.vault.previewDeposit(assetsAmount); - expect(response, "previewDeposit").to.eq("expected value"); - }); - - it('maxDeposit should ...', async () => { - const response = await ctx.vault.maxDeposit(ctx.owner.address); - expect(response, "maxDeposit").to.eq("expected value"); - }); - - it('previewMint should ...', async () => { - const response = await ctx.vault.previewMint(sharesAmount); - expect(response, "previewMint").to.eq("expected value"); - }); - - it('maxMint should ...', async () => { - const response = await ctx.vault.maxMint(ctx.owner.address); - expect(response, "maxMint").to.eq("expected value"); - }); - - - it('previewWithdraw should ...', async () => { - const response = await ctx.vault.previewWithdraw(assetsAmount); - expect(response, "previewWithdraw").to.eq("expected value"); - }); - - - it('maxWithdraw should ...', async () => { - const response = await ctx.vault.maxWithdraw(ctx.owner.address); - expect(response, "maxWithdraw").to.eq("expected value"); - }); - - it('previewRedeem should ...', async () => { - const response = await ctx.vault.previewRedeem(sharesAmount); - expect(response, "previewRedeem").to.eq("expected value"); - }); - - it('maxRedeem should ...', async () => { - const response = await ctx.vault.maxRedeem(ctx.owner.address); - expect(response, "maxRedeem").to.eq("expected value"); - }); - - it('totalAssets should ...', async () => { - const response = await ctx.vault.totalAssets(); - expect(response, "totalAssets").to.eq("expected value"); - }); - - - it('convertToAssets should ...', async () => { - const response = await ctx.vault.convertToAssets(sharesAmount); - expect(response, "convertToAssets").to.eq("expected value"); - }); - - - it('convertToShares should ...', async () => { - const response = await ctx.vault.convertToShares(assetsAmount); - expect(response, "convertToShares").to.eq("expected value"); - }); - - }); - - }); -} - -export default shouldBehaveLikeERC4626; From fca594148cbaf6202ab6b834155899c16dbe8e6c Mon Sep 17 00:00:00 2001 From: doncesarts Date: Wed, 13 Apr 2022 15:17:16 +0100 Subject: [PATCH 08/14] feat: implements IERC4626 on legacy SavingsContracts --- .../legacy-upgraded/imbtc-mainnet-22.sol | 617 +++++++++++++---- .../legacy-upgraded/imusd-mainnet-22.sol | 459 ++++++++++++- .../legacy-upgraded/imusd-polygon-22.sol | 618 ++++++++++++++---- contracts/savings/SavingsContract.sol | 14 +- test-fork/mUSD/aave2-migration.spec.ts | 4 +- .../savings/sc22-mainnet-upgrade.spec.ts | 444 +++++++++++-- .../savings/sc22-polygon-upgrade.spec.ts | 416 ++++++++++-- test-utils/math.ts | 2 + test/shared/ERC4626.behaviour.ts | 2 - 9 files changed, 2181 insertions(+), 395 deletions(-) diff --git a/contracts/legacy-upgraded/imbtc-mainnet-22.sol b/contracts/legacy-upgraded/imbtc-mainnet-22.sol index 80f84b7d..c8932275 100644 --- a/contracts/legacy-upgraded/imbtc-mainnet-22.sol +++ b/contracts/legacy-upgraded/imbtc-mainnet-22.sol @@ -1,6 +1,226 @@ // SPDX-License-Identifier: AGPL-3.0-or-later pragma solidity ^0.8.0; +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 IERC4626Vault is IERC20 { + /// @notice The address of the underlying token used for the Vault uses for accounting, depositing, and withdrawing + function asset() external view returns (address assetTokenAddress); + + /// @notice Total amount of the underlying asset that is “managed” by Vault + function totalAssets() external view returns (uint256 totalManagedAssets); + + /** + * @notice The amount of shares that the Vault would exchange for the amount of assets provided, in an ideal scenario where all the conditions are met. + * @param assets The amount of underlying assets to be convert to vault shares. + * @return shares The amount of vault shares converted from the underlying assets. + */ + function convertToShares(uint256 assets) external view returns (uint256 shares); + + /** + * @notice The amount of assets that the Vault would exchange for the amount of shares provided, in an ideal scenario where all the conditions are met. + * @param shares The amount of vault shares to be converted to the underlying assets. + * @return assets The amount of underlying assets converted from the vault shares. + */ + function convertToAssets(uint256 shares) external view returns (uint256 assets); + + /** + * @notice The maximum number of underlying assets that caller can deposit. + * @param caller Account that the assets will be transferred from. + * @return maxAssets The maximum amount of underlying assets the caller can deposit. + */ + function maxDeposit(address caller) external view returns (uint256 maxAssets); + + /** + * @notice Allows an on-chain or off-chain user to simulate the effects of their deposit at the current block, given current on-chain conditions. + * @param assets The amount of underlying assets to be transferred. + * @return shares The amount of vault shares that will be minted. + */ + function previewDeposit(uint256 assets) external view returns (uint256 shares); + + /** + * @notice Mint vault shares to receiver by transferring exact amount of underlying asset tokens from the caller. + * @param assets The amount of underlying assets to be transferred to the vault. + * @param receiver The account that the vault shares will be minted to. + * @return shares The amount of vault shares that were minted. + */ + function deposit(uint256 assets, address receiver) external returns (uint256 shares); + + /** + * @notice The maximum number of vault shares that caller can mint. + * @param caller Account that the underlying assets will be transferred from. + * @return maxShares The maximum amount of vault shares the caller can mint. + */ + function maxMint(address caller) external view returns (uint256 maxShares); + + /** + * @notice Allows an on-chain or off-chain user to simulate the effects of their mint at the current block, given current on-chain conditions. + * @param shares The amount of vault shares to be minted. + * @return assets The amount of underlying assests that will be transferred from the caller. + */ + function previewMint(uint256 shares) external view returns (uint256 assets); + + /** + * @notice Mint exact amount of vault shares to the receiver by transferring enough underlying asset tokens from the caller. + * @param shares The amount of vault shares to be minted. + * @param receiver The account the vault shares will be minted to. + * @return assets The amount of underlying assets that were transferred from the caller. + */ + function mint(uint256 shares, address receiver) external returns (uint256 assets); + + /** + * @notice The maximum number of underlying assets that owner can withdraw. + * @param owner Account that owns the vault shares. + * @return maxAssets The maximum amount of underlying assets the owner can withdraw. + */ + function maxWithdraw(address owner) external view returns (uint256 maxAssets); + + /** + * @notice Allows an on-chain or off-chain user to simulate the effects of their withdrawal at the current block, given current on-chain conditions. + * @param assets The amount of underlying assets to be withdrawn. + * @return shares The amount of vault shares that will be burnt. + */ + function previewWithdraw(uint256 assets) external view returns (uint256 shares); + + /** + * @notice Burns enough vault shares from owner and transfers the exact amount of underlying asset tokens to the receiver. + * @param assets The amount of underlying assets to be withdrawn from the vault. + * @param receiver The account that the underlying assets will be transferred to. + * @param owner Account that owns the vault shares to be burnt. + * @return shares The amount of vault shares that were burnt. + */ + function withdraw( + uint256 assets, + address receiver, + address owner + ) external returns (uint256 shares); + + /** + * @notice The maximum number of shares an owner can redeem for underlying assets. + * @param owner Account that owns the vault shares. + * @return maxShares The maximum amount of shares the owner can redeem. + */ + function maxRedeem(address owner) external view returns (uint256 maxShares); + + /** + * @notice Allows an on-chain or off-chain user to simulate the effects of their redeemption at the current block, given current on-chain conditions. + * @param shares The amount of vault shares to be burnt. + * @return assets The amount of underlying assests that will transferred to the receiver. + */ + function previewRedeem(uint256 shares) external view returns (uint256 assets); + + /** + * @notice Burns exact amount of vault shares from owner and transfers the underlying asset tokens to the receiver. + * @param shares The amount of vault shares to be burnt. + * @param receiver The account the underlying assets will be transferred to. + * @param owner The account that owns the vault shares to be burnt. + * @return assets The amount of underlying assets that were transferred to the receiver. + */ + function redeem( + uint256 shares, + address receiver, + address owner + ) external returns (uint256 assets); + + /*/////////////////////////////////////////////////////////////// + Events + //////////////////////////////////////////////////////////////*/ + + /** + * @dev Emitted when caller has exchanged assets for shares, and transferred those shares to owner. + * + * Note It must be emitted when tokens are deposited into the Vault in ERC4626.mint or ERC4626.deposit methods. + * + */ + event Deposit(address indexed caller, address indexed owner, uint256 assets, uint256 shares); + /** + * @dev Emitted when sender has exchanged shares for assets, and transferred those assets to receiver. + * + * Note It must be emitted when shares are withdrawn from the Vault in ERC4626.redeem or ERC4626.withdraw methods. + * + */ + event Withdraw( + address indexed caller, + address indexed receiver, + address indexed owner, + uint256 assets, + uint256 shares + ); +} + interface IUnwrapper { // @dev Get bAssetOut status function getIsBassetOut( @@ -124,81 +344,6 @@ abstract contract Context { } } -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); -} - contract ERC205 is Context, IERC20 { mapping(address => uint256) private _balances; @@ -927,11 +1072,12 @@ library StableMath { * @notice Savings contract uses the ever increasing "exchangeRate" to increase * the value of the Savers "credits" (ERC20) relative to the amount of additional * underlying collateral that has been deposited into this contract ("interest") - * @dev VERSION: 2.1 - * DATE: 2021-11-25 + * @dev VERSION: 2.2 + * DATE: 2022-04-08 */ contract SavingsContract_imbtc_mainnet_22 is ISavingsContractV3, + IERC4626Vault, Initializable, InitializableToken, ImmutableModule @@ -1175,24 +1321,7 @@ contract SavingsContract_imbtc_mainnet_22 is address _beneficiary, bool _collectInterest ) internal returns (uint256 creditsIssued) { - require(_underlying > 0, "Must deposit something"); - require(_beneficiary != address(0), "Invalid beneficiary address"); - - // Collect recent interest generated by basket and update exchange rate - IERC20 mAsset = underlying; - if (_collectInterest) { - ISavingsManager(_savingsManager()).collectAndDistributeInterest(address(mAsset)); - } - - // Transfer tokens from sender to here - require(mAsset.transferFrom(msg.sender, address(this), _underlying), "Must receive tokens"); - - // Calc how many credits they receive based on currentRatio - (creditsIssued, ) = _underlyingToCredits(_underlying); - - // add credits to ERC20 balances - _mint(_beneficiary, creditsIssued); - + creditsIssued = _transferAndMint(_underlying, _beneficiary, _collectInterest); emit SavingsDeposited(_beneficiary, _underlying, creditsIssued); } @@ -1353,21 +1482,14 @@ contract SavingsContract_imbtc_mainnet_22 is (credits_, exchangeRate_) = _underlyingToCredits(_amt); } - // Burn required credits from the sender FIRST - _burn(msg.sender, credits_); - // Optionally, transfer tokens from here to sender - if (_transferUnderlying) { - require(underlying.transfer(msg.sender, underlying_), "Must send tokens"); - } - // If this withdrawal pushes the portion of stored collateral in the `connector` over a certain - // threshold (fraction + 20%), then this should trigger a _poke on the connector. This is to avoid - // a situation in which there is a rush on withdrawals for some reason, causing the connector - // balance to go up and thus having too large an exposure. - CachedData memory cachedData = _cacheData(); - ConnectorStatus memory status = _getConnectorStatus(cachedData, exchangeRate_); - if (status.inConnector > status.limit) { - _poke(cachedData, false); - } + _burnTransfer( + underlying_, + credits_, + msg.sender, + msg.sender, + exchangeRate_, + _transferUnderlying + ); emit CreditsRedeemed(msg.sender, credits_, underlying_); @@ -1528,9 +1650,9 @@ contract SavingsContract_imbtc_mainnet_22 is uint256 ideal = sum.mulTruncate(_data.fraction); // If there is not enough mAsset in the connector, then deposit if (ideal > connectorBalance) { - uint256 deposit = ideal - connectorBalance; - underlying.approve(address(connector_), deposit); - connector_.deposit(deposit); + uint256 deposit_ = ideal - connectorBalance; + underlying.approve(address(connector_), deposit_); + connector_.deposit(deposit_); } // Else withdraw, if there is too much mAsset in the connector else if (connectorBalance > ideal) { @@ -1678,4 +1800,273 @@ contract SavingsContract_imbtc_mainnet_22 is exchangeRate_ = exchangeRate; underlyingAmount = _credits.mulTruncate(exchangeRate_); } + + /*/////////////////////////////////////////////////////////////// + IERC4626Vault + //////////////////////////////////////////////////////////////*/ + + /** + * @notice it must be an ERC-20 token contract. Must not revert. + * + * Returns the address of the underlying token used for the Vault uses for accounting, depositing, and withdrawing. + */ + function asset() external view override returns (address assetTokenAddress) { + return address(underlying); + } + + /** + * @notice The address of the underlying token used for the Vault uses for accounting, depositing, and withdrawing. + * Returns the total amount of the underlying asset that is “managed” by Vault. + */ + function totalAssets() external view override returns (uint256 totalManagedAssets) { + return underlying.balanceOf(address(this)); + } + + /** + * @notice The amount of shares that the Vault would exchange for the amount of assets provided, in an ideal scenario where all the conditions are met. + * @param assets The amount of underlying assets to be convert to vault shares. + * @return shares The amount of vault shares converted from the underlying assets. + */ + function convertToShares(uint256 assets) external view override returns (uint256 shares) { + (shares, ) = _underlyingToCredits(assets); + } + + /** + * @notice The amount of assets that the Vault would exchange for the amount of shares provided, in an ideal scenario where all the conditions are met. + * @param shares The amount of vault shares to be converted to the underlying assets. + * @return assets The amount of underlying assets converted from the vault shares. + */ + function convertToAssets(uint256 shares) external view override returns (uint256 assets) { + (assets, ) = _creditsToUnderlying(shares); + } + + /** + * @notice The maximum number of underlying assets that caller can deposit. + * caller Account that the assets will be transferred from. + * @return maxAssets The maximum amount of underlying assets the caller can deposit. + */ + function maxDeposit( + address /** caller **/ + ) external pure override returns (uint256 maxAssets) { + maxAssets = type(uint256).max; + } + + /** + * @notice Allows an on-chain or off-chain user to simulate the effects of their deposit at the current block, given current on-chain conditions. + * @param assets The amount of underlying assets to be transferred. + * @return shares The amount of vault shares that will be minted. + */ + function previewDeposit(uint256 assets) external view override returns (uint256 shares) { + require(assets > 0, "Must deposit something"); + (shares, ) = _underlyingToCredits(assets); + } + + /** + * @notice Mint vault shares to receiver by transferring exact amount of underlying asset tokens from the caller. + * Credit amount is calculated as a ratio of deposit amount and exchange rate: + * credits = underlying / exchangeRate + * We will first update the internal exchange rate by collecting any interest generated on the underlying. + * Emits a {Deposit} event. + * @param assets Units of underlying to deposit into savings vault. eg mUSD or mBTC + * @param receiver The address to receive the Vault shares. + * @return shares Units of credits issued. eg imUSD or imBTC + */ + function deposit(uint256 assets, address receiver) external override returns (uint256 shares) { + shares = _transferAndMint(assets, receiver, true); + } + + /** + * + * @notice Overloaded `deposit` method with an optional referrer address. + * @param assets Units of underlying to deposit into savings vault. eg mUSD or mBTC + * @param receiver Address to the new credits will be issued to. + * @param referrer Referrer address for this deposit. + * @return shares Units of credits issued. eg imUSD or imBTC + */ + function deposit( + uint256 assets, + address receiver, + address referrer + ) external returns (uint256 shares) { + shares = _transferAndMint(assets, receiver, true); + emit Referral(referrer, receiver, assets); + } + + /** + * @notice The maximum number of vault shares that caller can mint. + * caller Account that the underlying assets will be transferred from. + * @return maxShares The maximum amount of vault shares the caller can mint. + */ + function maxMint( + address /* caller */ + ) external pure override returns (uint256 maxShares) { + maxShares = type(uint256).max; + } + + /** + * @notice Allows an on-chain or off-chain user to simulate the effects of their mint at the current block, given current on-chain conditions. + * @param shares The amount of vault shares to be minted. + * @return assets The amount of underlying assests that will be transferred from the caller. + */ + function previewMint(uint256 shares) external view override returns (uint256 assets) { + (assets, ) = _creditsToUnderlying(shares); + return assets; + } + + /** + * @notice Mint exact amount of vault shares to the receiver by transferring enough underlying asset tokens from the caller. + * @param shares The amount of vault shares to be minted. + * @param receiver The account the vault shares will be minted to. + * @return assets The amount of underlying assets that were transferred from the caller. + * Emits a {Deposit} event. + */ + function mint(uint256 shares, address receiver) external override returns (uint256 assets) { + (assets, ) = _creditsToUnderlying(shares); + _transferAndMint(assets, receiver, true); + } + + /** + * + * Returns Total number of underlying assets that caller can withdraw. + */ + function maxWithdraw(address caller) external view override returns (uint256 maxAssets) { + (maxAssets, ) = _creditsToUnderlying(balanceOf(caller)); + } + + /** + * @notice Allows an on-chain or off-chain user to simulate the effects of their withdrawal at the current block, given current on-chain conditions. + * + * Return the exact amount of Vault shares that would be redeemed by the caller if withdrawing a given exact amount of underlying assets using the withdraw method. + */ + function previewWithdraw(uint256 assets) external view override returns (uint256 shares) { + (shares, ) = _underlyingToCredits(assets); + } + + /** + * Redeems shares from owner and sends assets of underlying tokens to receiver. + * Returns Total number of underlying shares redeemed. + * Emits a {Withdraw} event. + */ + function withdraw( + uint256 assets, + address receiver, + address owner + ) external override returns (uint256 shares) { + require(assets > 0, "Must withdraw something"); + uint256 _exchangeRate; + if (automateInterestCollection) { + ISavingsManager(_savingsManager()).collectAndDistributeInterest(address(underlying)); + } + (shares, _exchangeRate) = _underlyingToCredits(assets); + + _burnTransfer(assets, shares, receiver, owner, _exchangeRate, true); + } + + /** + * @notice it must return a limited value if caller is subject to some withdrawal limit or timelock. must return balanceOf(caller) if caller is not subject to any withdrawal limit or timelock. MAY be used in the previewRedeem or redeem methods for shares input parameter. must NOT revert. + * + * Returns Total number of underlying shares that caller can redeem. + */ + function maxRedeem(address caller) external view override returns (uint256 maxShares) { + maxShares = balanceOf(caller); + } + + /** + * @notice Allows an on-chain or off-chain user to simulate the effects of their redeemption at the current block, given current on-chain conditions. + * + * Returns the exact amount of underlying assets that would be withdrawn by the caller if redeeming a given exact amount of Vault shares using the redeem method + */ + function previewRedeem(uint256 shares) external view override returns (uint256 assets) { + (assets, ) = _creditsToUnderlying(shares); + return assets; + } + + /** + * Redeems shares from owner and sends assets of underlying tokens to receiver. + * + * Returns Total number of underlying assets of underlying redeemed. + * Emits a {Withdraw} event. + */ + function redeem( + uint256 shares, + address receiver, + address owner + ) external override returns (uint256 assets) { + require(shares > 0, "Must withdraw something"); + uint256 _exchangeRate; + if (automateInterestCollection) { + ISavingsManager(_savingsManager()).collectAndDistributeInterest(address(underlying)); + } + (assets, _exchangeRate) = _creditsToUnderlying(shares); + + _burnTransfer(assets, shares, receiver, owner, _exchangeRate, true); //transferAssets=true + } + + /*/////////////////////////////////////////////////////////////// + INTERNAL DEPOSIT/MINT + //////////////////////////////////////////////////////////////*/ + function _transferAndMint( + uint256 assets, + address receiver, + bool _collectInterest + ) internal returns (uint256 shares) { + require(assets > 0, "Must deposit something"); + require(receiver != address(0), "Invalid beneficiary address"); + + // Collect recent interest generated by basket and update exchange rate + IERC20 mAsset = underlying; + if (_collectInterest) { + ISavingsManager(_savingsManager()).collectAndDistributeInterest(address(mAsset)); + } + + // Transfer tokens from sender to here + require(mAsset.transferFrom(msg.sender, address(this), assets), "Must receive tokens"); + + // Calc how many credits they receive based on currentRatio + (shares, ) = _underlyingToCredits(assets); + + // add credits to ERC20 balances + _mint(receiver, shares); + emit Deposit(msg.sender, receiver, assets, shares); + } + + /*/////////////////////////////////////////////////////////////// + INTERNAL WITHDRAW/REDEEM + //////////////////////////////////////////////////////////////*/ + + function _burnTransfer( + uint256 assets, + uint256 shares, + address receiver, + address owner, + uint256 _exchangeRate, + bool transferAssets + ) internal { + require(receiver != address(0), "Invalid beneficiary address"); + + // If caller is not the owner of the shares + uint256 allowed = allowance(owner, msg.sender); + if (msg.sender != owner && allowed != type(uint256).max) { + require(shares <= allowed, "Amount exceeds allowance"); + _approve(owner, msg.sender, allowed - shares); + } + + // Burn required shares from the owner FIRST + _burn(owner, shares); + + // Optionally, transfer tokens from here to receiver + if (transferAssets) { + require(underlying.transfer(receiver, assets), "Must send tokens"); + emit Withdraw(msg.sender, receiver, owner, assets, shares); + } + // If this withdrawal pushes the portion of stored collateral in the `connector` over a certain + // threshold (fraction + 20%), then this should trigger a _poke on the connector. This is to avoid + // a situation in which there is a rush on withdrawals for some reason, causing the connector + // balance to go up and thus having too large an exposure. + CachedData memory cachedData = _cacheData(); + ConnectorStatus memory status = _getConnectorStatus(cachedData, _exchangeRate); + if (status.inConnector > status.limit) { + _poke(cachedData, false); + } + } } diff --git a/contracts/legacy-upgraded/imusd-mainnet-22.sol b/contracts/legacy-upgraded/imusd-mainnet-22.sol index daed924c..4434bde1 100644 --- a/contracts/legacy-upgraded/imusd-mainnet-22.sol +++ b/contracts/legacy-upgraded/imusd-mainnet-22.sol @@ -1,5 +1,151 @@ pragma solidity 0.5.16; +/* is IERC20 */ +interface IERC4626Vault { + /// @notice The address of the underlying token used for the Vault uses for accounting, depositing, and withdrawing + function asset() external view returns (address assetTokenAddress); + + /// @notice Total amount of the underlying asset that is “managed” by Vault + function totalAssets() external view returns (uint256 totalManagedAssets); + + /** + * @notice The amount of shares that the Vault would exchange for the amount of assets provided, in an ideal scenario where all the conditions are met. + * @param assets The amount of underlying assets to be convert to vault shares. + * @return shares The amount of vault shares converted from the underlying assets. + */ + function convertToShares(uint256 assets) external view returns (uint256 shares); + + /** + * @notice The amount of assets that the Vault would exchange for the amount of shares provided, in an ideal scenario where all the conditions are met. + * @param shares The amount of vault shares to be converted to the underlying assets. + * @return assets The amount of underlying assets converted from the vault shares. + */ + function convertToAssets(uint256 shares) external view returns (uint256 assets); + + /** + * @notice The maximum number of underlying assets that caller can deposit. + * @param caller Account that the assets will be transferred from. + * @return maxAssets The maximum amount of underlying assets the caller can deposit. + */ + function maxDeposit(address caller) external view returns (uint256 maxAssets); + + /** + * @notice Allows an on-chain or off-chain user to simulate the effects of their deposit at the current block, given current on-chain conditions. + * @param assets The amount of underlying assets to be transferred. + * @return shares The amount of vault shares that will be minted. + */ + function previewDeposit(uint256 assets) external view returns (uint256 shares); + + /** + * @notice Mint vault shares to receiver by transferring exact amount of underlying asset tokens from the caller. + * @param assets The amount of underlying assets to be transferred to the vault. + * @param receiver The account that the vault shares will be minted to. + * @return shares The amount of vault shares that were minted. + */ + function deposit(uint256 assets, address receiver) external returns (uint256 shares); + + /** + * @notice The maximum number of vault shares that caller can mint. + * @param caller Account that the underlying assets will be transferred from. + * @return maxShares The maximum amount of vault shares the caller can mint. + */ + function maxMint(address caller) external view returns (uint256 maxShares); + + /** + * @notice Allows an on-chain or off-chain user to simulate the effects of their mint at the current block, given current on-chain conditions. + * @param shares The amount of vault shares to be minted. + * @return assets The amount of underlying assests that will be transferred from the caller. + */ + function previewMint(uint256 shares) external view returns (uint256 assets); + + /** + * @notice Mint exact amount of vault shares to the receiver by transferring enough underlying asset tokens from the caller. + * @param shares The amount of vault shares to be minted. + * @param receiver The account the vault shares will be minted to. + * @return assets The amount of underlying assets that were transferred from the caller. + */ + function mint(uint256 shares, address receiver) external returns (uint256 assets); + + /** + * @notice The maximum number of underlying assets that owner can withdraw. + * @param owner Account that owns the vault shares. + * @return maxAssets The maximum amount of underlying assets the owner can withdraw. + */ + function maxWithdraw(address owner) external view returns (uint256 maxAssets); + + /** + * @notice Allows an on-chain or off-chain user to simulate the effects of their withdrawal at the current block, given current on-chain conditions. + * @param assets The amount of underlying assets to be withdrawn. + * @return shares The amount of vault shares that will be burnt. + */ + function previewWithdraw(uint256 assets) external view returns (uint256 shares); + + /** + * @notice Burns enough vault shares from owner and transfers the exact amount of underlying asset tokens to the receiver. + * @param assets The amount of underlying assets to be withdrawn from the vault. + * @param receiver The account that the underlying assets will be transferred to. + * @param owner Account that owns the vault shares to be burnt. + * @return shares The amount of vault shares that were burnt. + */ + function withdraw( + uint256 assets, + address receiver, + address owner + ) external returns (uint256 shares); + + /** + * @notice The maximum number of shares an owner can redeem for underlying assets. + * @param owner Account that owns the vault shares. + * @return maxShares The maximum amount of shares the owner can redeem. + */ + function maxRedeem(address owner) external view returns (uint256 maxShares); + + /** + * @notice Allows an on-chain or off-chain user to simulate the effects of their redeemption at the current block, given current on-chain conditions. + * @param shares The amount of vault shares to be burnt. + * @return assets The amount of underlying assests that will transferred to the receiver. + */ + function previewRedeem(uint256 shares) external view returns (uint256 assets); + + /** + * @notice Burns exact amount of vault shares from owner and transfers the underlying asset tokens to the receiver. + * @param shares The amount of vault shares to be burnt. + * @param receiver The account the underlying assets will be transferred to. + * @param owner The account that owns the vault shares to be burnt. + * @return assets The amount of underlying assets that were transferred to the receiver. + */ + function redeem( + uint256 shares, + address receiver, + address owner + ) external returns (uint256 assets); + + /*/////////////////////////////////////////////////////////////// + Events + //////////////////////////////////////////////////////////////*/ + + /** + * @dev Emitted when caller has exchanged assets for shares, and transferred those shares to owner. + * + * Note It must be emitted when tokens are deposited into the Vault in ERC4626.mint or ERC4626.deposit methods. + * + */ + event Deposit(address indexed caller, address indexed owner, uint256 assets, uint256 shares); + /** + * @dev Emitted when sender has exchanged shares for assets, and transferred those assets to receiver. + * + * Note It must be emitted when shares are withdrawn from the Vault in ERC4626.redeem or ERC4626.withdraw methods. + * + */ + event Withdraw( + address indexed caller, + address indexed receiver, + address indexed owner, + uint256 assets, + uint256 shares + ); +} + interface IUnwrapper { // @dev Get bAssetOut status function getIsBassetOut( @@ -1128,6 +1274,7 @@ library StableMath { contract SavingsContract_imusd_mainnet_22 is ISavingsContractV1, ISavingsContractV3, + IERC4626Vault, Initializable, InitializableToken, InitializableModule2 @@ -1355,24 +1502,7 @@ contract SavingsContract_imusd_mainnet_22 is address _beneficiary, bool _collectInterest ) internal returns (uint256 creditsIssued) { - require(_underlying > 0, "Must deposit something"); - require(_beneficiary != address(0), "Invalid beneficiary address"); - - // Collect recent interest generated by basket and update exchange rate - IERC20 mAsset = underlying; - if (_collectInterest) { - ISavingsManager(_savingsManager()).collectAndDistributeInterest(address(mAsset)); - } - - // Transfer tokens from sender to here - require(mAsset.transferFrom(msg.sender, address(this), _underlying), "Must receive tokens"); - - // Calc how many credits they receive based on currentRatio - (creditsIssued, ) = _underlyingToCredits(_underlying); - - // add credits to ERC20 balances - _mint(_beneficiary, creditsIssued); - + creditsIssued = _transferAndMint(_underlying, _beneficiary, _collectInterest); emit SavingsDeposited(_beneficiary, _underlying, creditsIssued); } @@ -1528,22 +1658,14 @@ contract SavingsContract_imusd_mainnet_22 is (credits_, exchangeRate_) = _underlyingToCredits(_amt); } - // Burn required credits from the sender FIRST - _burn(msg.sender, credits_); - // Optionally, transfer tokens from here to sender - if (_transferUnderlying) { - require(underlying.transfer(msg.sender, underlying_), "Must send tokens"); - } - - // If this withdrawal pushes the portion of stored collateral in the `connector` over a certain - // threshold (fraction + 20%), then this should trigger a _poke on the connector. This is to avoid - // a situation in which there is a rush on withdrawals for some reason, causing the connector - // balance to go up and thus having too large an exposure. - CachedData memory cachedData = _cacheData(); - ConnectorStatus memory status = _getConnectorStatus(cachedData, exchangeRate_); - if (status.inConnector > status.limit) { - _poke(cachedData, false); - } + _burnTransfer( + underlying_, + credits_, + msg.sender, + msg.sender, + exchangeRate_, + _transferUnderlying + ); emit CreditsRedeemed(msg.sender, credits_, underlying_); @@ -1856,4 +1978,273 @@ contract SavingsContract_imusd_mainnet_22 is exchangeRate_ = exchangeRate; underlyingAmount = _credits.mulTruncate(exchangeRate_); } + + /*/////////////////////////////////////////////////////////////// + IERC4626Vault + //////////////////////////////////////////////////////////////*/ + + /** + * @notice it must be an ERC-20 token contract. Must not revert. + * + * Returns the address of the underlying token used for the Vault uses for accounting, depositing, and withdrawing. + */ + function asset() external view returns (address assetTokenAddress) { + return address(underlying); + } + + /** + * @notice The address of the underlying token used for the Vault uses for accounting, depositing, and withdrawing. + * Returns the total amount of the underlying asset that is “managed” by Vault. + */ + function totalAssets() external view returns (uint256 totalManagedAssets) { + return underlying.balanceOf(address(this)); + } + + /** + * @notice The amount of shares that the Vault would exchange for the amount of assets provided, in an ideal scenario where all the conditions are met. + * @param assets The amount of underlying assets to be convert to vault shares. + * @return shares The amount of vault shares converted from the underlying assets. + */ + function convertToShares(uint256 assets) external view returns (uint256 shares) { + (shares, ) = _underlyingToCredits(assets); + } + + /** + * @notice The amount of assets that the Vault would exchange for the amount of shares provided, in an ideal scenario where all the conditions are met. + * @param shares The amount of vault shares to be converted to the underlying assets. + * @return assets The amount of underlying assets converted from the vault shares. + */ + function convertToAssets(uint256 shares) external view returns (uint256 assets) { + (assets, ) = _creditsToUnderlying(shares); + } + + /** + * @notice The maximum number of underlying assets that caller can deposit. + * caller Account that the assets will be transferred from. + * @return maxAssets The maximum amount of underlying assets the caller can deposit. + */ + function maxDeposit( + address /** caller **/ + ) external view returns (uint256 maxAssets) { + maxAssets = 2**256 - 1; + } + + /** + * @notice Allows an on-chain or off-chain user to simulate the effects of their deposit at the current block, given current on-chain conditions. + * @param assets The amount of underlying assets to be transferred. + * @return shares The amount of vault shares that will be minted. + */ + function previewDeposit(uint256 assets) external view returns (uint256 shares) { + require(assets > 0, "Must deposit something"); + (shares, ) = _underlyingToCredits(assets); + } + + /** + * @notice Mint vault shares to receiver by transferring exact amount of underlying asset tokens from the caller. + * Credit amount is calculated as a ratio of deposit amount and exchange rate: + * credits = underlying / exchangeRate + * We will first update the internal exchange rate by collecting any interest generated on the underlying. + * Emits a {Deposit} event. + * @param assets Units of underlying to deposit into savings vault. eg mUSD or mBTC + * @param receiver The address to receive the Vault shares. + * @return shares Units of credits issued. eg imUSD or imBTC + */ + function deposit(uint256 assets, address receiver) external returns (uint256 shares) { + shares = _transferAndMint(assets, receiver, true); + } + + /** + * + * @notice Overloaded `deposit` method with an optional referrer address. + * @param assets Units of underlying to deposit into savings vault. eg mUSD or mBTC + * @param receiver Address to the new credits will be issued to. + * @param referrer Referrer address for this deposit. + * @return shares Units of credits issued. eg imUSD or imBTC + */ + function deposit( + uint256 assets, + address receiver, + address referrer + ) external returns (uint256 shares) { + shares = _transferAndMint(assets, receiver, true); + emit Referral(referrer, receiver, assets); + } + + /** + * @notice The maximum number of vault shares that caller can mint. + * caller Account that the underlying assets will be transferred from. + * @return maxShares The maximum amount of vault shares the caller can mint. + */ + function maxMint( + address /* caller */ + ) external view returns (uint256 maxShares) { + maxShares = 2**256 - 1; + } + + /** + * @notice Allows an on-chain or off-chain user to simulate the effects of their mint at the current block, given current on-chain conditions. + * @param shares The amount of vault shares to be minted. + * @return assets The amount of underlying assests that will be transferred from the caller. + */ + function previewMint(uint256 shares) external view returns (uint256 assets) { + (assets, ) = _creditsToUnderlying(shares); + return assets; + } + + /** + * @notice Mint exact amount of vault shares to the receiver by transferring enough underlying asset tokens from the caller. + * @param shares The amount of vault shares to be minted. + * @param receiver The account the vault shares will be minted to. + * @return assets The amount of underlying assets that were transferred from the caller. + * Emits a {Deposit} event. + */ + function mint(uint256 shares, address receiver) external returns (uint256 assets) { + (assets, ) = _creditsToUnderlying(shares); + _transferAndMint(assets, receiver, true); + } + + /** + * + * Returns Total number of underlying assets that caller can withdraw. + */ + function maxWithdraw(address caller) external view returns (uint256 maxAssets) { + (maxAssets, ) = _creditsToUnderlying(balanceOf(caller)); + } + + /** + * @notice Allows an on-chain or off-chain user to simulate the effects of their withdrawal at the current block, given current on-chain conditions. + * + * Return the exact amount of Vault shares that would be redeemed by the caller if withdrawing a given exact amount of underlying assets using the withdraw method. + */ + function previewWithdraw(uint256 assets) external view returns (uint256 shares) { + (shares, ) = _underlyingToCredits(assets); + } + + /** + * Redeems shares from owner and sends assets of underlying tokens to receiver. + * Returns Total number of underlying shares redeemed. + * Emits a {Withdraw} event. + */ + function withdraw( + uint256 assets, + address receiver, + address owner + ) external returns (uint256 shares) { + require(assets > 0, "Must withdraw something"); + uint256 _exchangeRate; + if (automateInterestCollection) { + ISavingsManager(_savingsManager()).collectAndDistributeInterest(address(underlying)); + } + (shares, _exchangeRate) = _underlyingToCredits(assets); + + _burnTransfer(assets, shares, receiver, owner, _exchangeRate, true); + } + + /** + * @notice it must return a limited value if caller is subject to some withdrawal limit or timelock. must return balanceOf(caller) if caller is not subject to any withdrawal limit or timelock. MAY be used in the previewRedeem or redeem methods for shares input parameter. must NOT revert. + * + * Returns Total number of underlying shares that caller can redeem. + */ + function maxRedeem(address caller) external view returns (uint256 maxShares) { + maxShares = balanceOf(caller); + } + + /** + * @notice Allows an on-chain or off-chain user to simulate the effects of their redeemption at the current block, given current on-chain conditions. + * + * Returns the exact amount of underlying assets that would be withdrawn by the caller if redeeming a given exact amount of Vault shares using the redeem method + */ + function previewRedeem(uint256 shares) external view returns (uint256 assets) { + (assets, ) = _creditsToUnderlying(shares); + return assets; + } + + /** + * Redeems shares from owner and sends assets of underlying tokens to receiver. + * + * Returns Total number of underlying assets of underlying redeemed. + * Emits a {Withdraw} event. + */ + function redeem( + uint256 shares, + address receiver, + address owner + ) external returns (uint256 assets) { + require(shares > 0, "Must withdraw something"); + uint256 _exchangeRate; + if (automateInterestCollection) { + ISavingsManager(_savingsManager()).collectAndDistributeInterest(address(underlying)); + } + (assets, _exchangeRate) = _creditsToUnderlying(shares); + + _burnTransfer(assets, shares, receiver, owner, _exchangeRate, true); //transferAssets=true + } + + /*/////////////////////////////////////////////////////////////// + INTERNAL DEPOSIT/MINT + //////////////////////////////////////////////////////////////*/ + function _transferAndMint( + uint256 assets, + address receiver, + bool _collectInterest + ) internal returns (uint256 shares) { + require(assets > 0, "Must deposit something"); + require(receiver != address(0), "Invalid beneficiary address"); + + // Collect recent interest generated by basket and update exchange rate + IERC20 mAsset = underlying; + if (_collectInterest) { + ISavingsManager(_savingsManager()).collectAndDistributeInterest(address(mAsset)); + } + + // Transfer tokens from sender to here + require(mAsset.transferFrom(msg.sender, address(this), assets), "Must receive tokens"); + + // Calc how many credits they receive based on currentRatio + (shares, ) = _underlyingToCredits(assets); + + // add credits to ERC20 balances + _mint(receiver, shares); + emit Deposit(msg.sender, receiver, assets, shares); + } + + /*/////////////////////////////////////////////////////////////// + INTERNAL WITHDRAW/REDEEM + //////////////////////////////////////////////////////////////*/ + + function _burnTransfer( + uint256 assets, + uint256 shares, + address receiver, + address owner, + uint256 _exchangeRate, + bool transferAssets + ) internal { + require(receiver != address(0), "Invalid beneficiary address"); + + // If caller is not the owner of the shares + uint256 allowed = allowance(owner, msg.sender); + if (msg.sender != owner && allowed != (2**256 - 1)) { + require(shares <= allowed, "Amount exceeds allowance"); + _approve(owner, msg.sender, allowed - shares); + } + + // Burn required shares from the owner FIRST + _burn(owner, shares); + + // Optionally, transfer tokens from here to receiver + if (transferAssets) { + require(underlying.transfer(receiver, assets), "Must send tokens"); + emit Withdraw(msg.sender, receiver, owner, assets, shares); + } + // If this withdrawal pushes the portion of stored collateral in the `connector` over a certain + // threshold (fraction + 20%), then this should trigger a _poke on the connector. This is to avoid + // a situation in which there is a rush on withdrawals for some reason, causing the connector + // balance to go up and thus having too large an exposure. + CachedData memory cachedData = _cacheData(); + ConnectorStatus memory status = _getConnectorStatus(cachedData, _exchangeRate); + if (status.inConnector > status.limit) { + _poke(cachedData, false); + } + } } diff --git a/contracts/legacy-upgraded/imusd-polygon-22.sol b/contracts/legacy-upgraded/imusd-polygon-22.sol index 0bbfc23b..5372c6be 100644 --- a/contracts/legacy-upgraded/imusd-polygon-22.sol +++ b/contracts/legacy-upgraded/imusd-polygon-22.sol @@ -1,6 +1,226 @@ // SPDX-License-Identifier: AGPL-3.0-or-later pragma solidity 0.8.2; +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 IERC4626Vault is IERC20 { + /// @notice The address of the underlying token used for the Vault uses for accounting, depositing, and withdrawing + function asset() external view returns (address assetTokenAddress); + + /// @notice Total amount of the underlying asset that is “managed” by Vault + function totalAssets() external view returns (uint256 totalManagedAssets); + + /** + * @notice The amount of shares that the Vault would exchange for the amount of assets provided, in an ideal scenario where all the conditions are met. + * @param assets The amount of underlying assets to be convert to vault shares. + * @return shares The amount of vault shares converted from the underlying assets. + */ + function convertToShares(uint256 assets) external view returns (uint256 shares); + + /** + * @notice The amount of assets that the Vault would exchange for the amount of shares provided, in an ideal scenario where all the conditions are met. + * @param shares The amount of vault shares to be converted to the underlying assets. + * @return assets The amount of underlying assets converted from the vault shares. + */ + function convertToAssets(uint256 shares) external view returns (uint256 assets); + + /** + * @notice The maximum number of underlying assets that caller can deposit. + * @param caller Account that the assets will be transferred from. + * @return maxAssets The maximum amount of underlying assets the caller can deposit. + */ + function maxDeposit(address caller) external view returns (uint256 maxAssets); + + /** + * @notice Allows an on-chain or off-chain user to simulate the effects of their deposit at the current block, given current on-chain conditions. + * @param assets The amount of underlying assets to be transferred. + * @return shares The amount of vault shares that will be minted. + */ + function previewDeposit(uint256 assets) external view returns (uint256 shares); + + /** + * @notice Mint vault shares to receiver by transferring exact amount of underlying asset tokens from the caller. + * @param assets The amount of underlying assets to be transferred to the vault. + * @param receiver The account that the vault shares will be minted to. + * @return shares The amount of vault shares that were minted. + */ + function deposit(uint256 assets, address receiver) external returns (uint256 shares); + + /** + * @notice The maximum number of vault shares that caller can mint. + * @param caller Account that the underlying assets will be transferred from. + * @return maxShares The maximum amount of vault shares the caller can mint. + */ + function maxMint(address caller) external view returns (uint256 maxShares); + + /** + * @notice Allows an on-chain or off-chain user to simulate the effects of their mint at the current block, given current on-chain conditions. + * @param shares The amount of vault shares to be minted. + * @return assets The amount of underlying assests that will be transferred from the caller. + */ + function previewMint(uint256 shares) external view returns (uint256 assets); + + /** + * @notice Mint exact amount of vault shares to the receiver by transferring enough underlying asset tokens from the caller. + * @param shares The amount of vault shares to be minted. + * @param receiver The account the vault shares will be minted to. + * @return assets The amount of underlying assets that were transferred from the caller. + */ + function mint(uint256 shares, address receiver) external returns (uint256 assets); + + /** + * @notice The maximum number of underlying assets that owner can withdraw. + * @param owner Account that owns the vault shares. + * @return maxAssets The maximum amount of underlying assets the owner can withdraw. + */ + function maxWithdraw(address owner) external view returns (uint256 maxAssets); + + /** + * @notice Allows an on-chain or off-chain user to simulate the effects of their withdrawal at the current block, given current on-chain conditions. + * @param assets The amount of underlying assets to be withdrawn. + * @return shares The amount of vault shares that will be burnt. + */ + function previewWithdraw(uint256 assets) external view returns (uint256 shares); + + /** + * @notice Burns enough vault shares from owner and transfers the exact amount of underlying asset tokens to the receiver. + * @param assets The amount of underlying assets to be withdrawn from the vault. + * @param receiver The account that the underlying assets will be transferred to. + * @param owner Account that owns the vault shares to be burnt. + * @return shares The amount of vault shares that were burnt. + */ + function withdraw( + uint256 assets, + address receiver, + address owner + ) external returns (uint256 shares); + + /** + * @notice The maximum number of shares an owner can redeem for underlying assets. + * @param owner Account that owns the vault shares. + * @return maxShares The maximum amount of shares the owner can redeem. + */ + function maxRedeem(address owner) external view returns (uint256 maxShares); + + /** + * @notice Allows an on-chain or off-chain user to simulate the effects of their redeemption at the current block, given current on-chain conditions. + * @param shares The amount of vault shares to be burnt. + * @return assets The amount of underlying assests that will transferred to the receiver. + */ + function previewRedeem(uint256 shares) external view returns (uint256 assets); + + /** + * @notice Burns exact amount of vault shares from owner and transfers the underlying asset tokens to the receiver. + * @param shares The amount of vault shares to be burnt. + * @param receiver The account the underlying assets will be transferred to. + * @param owner The account that owns the vault shares to be burnt. + * @return assets The amount of underlying assets that were transferred to the receiver. + */ + function redeem( + uint256 shares, + address receiver, + address owner + ) external returns (uint256 assets); + + /*/////////////////////////////////////////////////////////////// + Events + //////////////////////////////////////////////////////////////*/ + + /** + * @dev Emitted when caller has exchanged assets for shares, and transferred those shares to owner. + * + * Note It must be emitted when tokens are deposited into the Vault in ERC4626.mint or ERC4626.deposit methods. + * + */ + event Deposit(address indexed caller, address indexed owner, uint256 assets, uint256 shares); + /** + * @dev Emitted when sender has exchanged shares for assets, and transferred those assets to receiver. + * + * Note It must be emitted when shares are withdrawn from the Vault in ERC4626.redeem or ERC4626.withdraw methods. + * + */ + event Withdraw( + address indexed caller, + address indexed receiver, + address indexed owner, + uint256 assets, + uint256 shares + ); +} + interface IUnwrapper { // @dev Get bAssetOut status function getIsBassetOut( @@ -127,81 +347,6 @@ abstract contract Context { } } -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); -} - contract ERC205 is Context, IERC20 { mapping(address => uint256) private _balances; @@ -973,11 +1118,12 @@ library YieldValidator { * @notice Savings contract uses the ever increasing "exchangeRate" to increase * the value of the Savers "credits" (ERC20) relative to the amount of additional * underlying collateral that has been deposited into this contract ("interest") - * @dev VERSION: 2.1 - * DATE: 2021-11-25 + * @dev VERSION: 2.2 + * DATE: 2022-04-08 */ contract SavingsContract_imusd_polygon_22 is ISavingsContractV3, + IERC4626Vault, Initializable, InitializableToken, ImmutableModule @@ -1221,24 +1367,7 @@ contract SavingsContract_imusd_polygon_22 is address _beneficiary, bool _collectInterest ) internal returns (uint256 creditsIssued) { - require(_underlying > 0, "Must deposit something"); - require(_beneficiary != address(0), "Invalid beneficiary address"); - - // Collect recent interest generated by basket and update exchange rate - IERC20 mAsset = underlying; - if (_collectInterest) { - ISavingsManager(_savingsManager()).collectAndDistributeInterest(address(mAsset)); - } - - // Transfer tokens from sender to here - require(mAsset.transferFrom(msg.sender, address(this), _underlying), "Must receive tokens"); - - // Calc how many credits they receive based on currentRatio - (creditsIssued, ) = _underlyingToCredits(_underlying); - - // add credits to ERC20 balances - _mint(_beneficiary, creditsIssued); - + creditsIssued = _transferAndMint(_underlying, _beneficiary, _collectInterest); emit SavingsDeposited(_beneficiary, _underlying, creditsIssued); } @@ -1399,22 +1528,14 @@ contract SavingsContract_imusd_polygon_22 is (credits_, exchangeRate_) = _underlyingToCredits(_amt); } - // Burn required credits from the sender FIRST - _burn(msg.sender, credits_); - // Optionally, transfer tokens from here to sender - if (_transferUnderlying) { - require(underlying.transfer(msg.sender, underlying_), "Must send tokens"); - } - - // If this withdrawal pushes the portion of stored collateral in the `connector` over a certain - // threshold (fraction + 20%), then this should trigger a _poke on the connector. This is to avoid - // a situation in which there is a rush on withdrawals for some reason, causing the connector - // balance to go up and thus having too large an exposure. - CachedData memory cachedData = _cacheData(); - ConnectorStatus memory status = _getConnectorStatus(cachedData, exchangeRate_); - if (status.inConnector > status.limit) { - _poke(cachedData, false); - } + _burnTransfer( + underlying_, + credits_, + msg.sender, + msg.sender, + exchangeRate_, + _transferUnderlying + ); emit CreditsRedeemed(msg.sender, credits_, underlying_); @@ -1577,9 +1698,9 @@ contract SavingsContract_imusd_polygon_22 is uint256 ideal = sum.mulTruncate(_data.fraction); // If there is not enough mAsset in the connector, then deposit if (ideal > connectorBalance) { - uint256 deposit = ideal - connectorBalance; - underlying.approve(address(connector_), deposit); - connector_.deposit(deposit); + uint256 deposit_ = ideal - connectorBalance; + underlying.approve(address(connector_), deposit_); + connector_.deposit(deposit_); } // Else withdraw, if there is too much mAsset in the connector else if (connectorBalance > ideal) { @@ -1696,4 +1817,273 @@ contract SavingsContract_imusd_polygon_22 is exchangeRate_ = exchangeRate; underlyingAmount = _credits.mulTruncate(exchangeRate_); } + + /*/////////////////////////////////////////////////////////////// + IERC4626Vault + //////////////////////////////////////////////////////////////*/ + + /** + * @notice it must be an ERC-20 token contract. Must not revert. + * + * Returns the address of the underlying token used for the Vault uses for accounting, depositing, and withdrawing. + */ + function asset() external view override returns (address assetTokenAddress) { + return address(underlying); + } + + /** + * @notice The address of the underlying token used for the Vault uses for accounting, depositing, and withdrawing. + * Returns the total amount of the underlying asset that is “managed” by Vault. + */ + function totalAssets() external view override returns (uint256 totalManagedAssets) { + return underlying.balanceOf(address(this)); + } + + /** + * @notice The amount of shares that the Vault would exchange for the amount of assets provided, in an ideal scenario where all the conditions are met. + * @param assets The amount of underlying assets to be convert to vault shares. + * @return shares The amount of vault shares converted from the underlying assets. + */ + function convertToShares(uint256 assets) external view override returns (uint256 shares) { + (shares, ) = _underlyingToCredits(assets); + } + + /** + * @notice The amount of assets that the Vault would exchange for the amount of shares provided, in an ideal scenario where all the conditions are met. + * @param shares The amount of vault shares to be converted to the underlying assets. + * @return assets The amount of underlying assets converted from the vault shares. + */ + function convertToAssets(uint256 shares) external view override returns (uint256 assets) { + (assets, ) = _creditsToUnderlying(shares); + } + + /** + * @notice The maximum number of underlying assets that caller can deposit. + * caller Account that the assets will be transferred from. + * @return maxAssets The maximum amount of underlying assets the caller can deposit. + */ + function maxDeposit( + address /** caller **/ + ) external pure override returns (uint256 maxAssets) { + maxAssets = type(uint256).max; + } + + /** + * @notice Allows an on-chain or off-chain user to simulate the effects of their deposit at the current block, given current on-chain conditions. + * @param assets The amount of underlying assets to be transferred. + * @return shares The amount of vault shares that will be minted. + */ + function previewDeposit(uint256 assets) external view override returns (uint256 shares) { + require(assets > 0, "Must deposit something"); + (shares, ) = _underlyingToCredits(assets); + } + + /** + * @notice Mint vault shares to receiver by transferring exact amount of underlying asset tokens from the caller. + * Credit amount is calculated as a ratio of deposit amount and exchange rate: + * credits = underlying / exchangeRate + * We will first update the internal exchange rate by collecting any interest generated on the underlying. + * Emits a {Deposit} event. + * @param assets Units of underlying to deposit into savings vault. eg mUSD or mBTC + * @param receiver The address to receive the Vault shares. + * @return shares Units of credits issued. eg imUSD or imBTC + */ + function deposit(uint256 assets, address receiver) external override returns (uint256 shares) { + shares = _transferAndMint(assets, receiver, true); + } + + /** + * + * @notice Overloaded `deposit` method with an optional referrer address. + * @param assets Units of underlying to deposit into savings vault. eg mUSD or mBTC + * @param receiver Address to the new credits will be issued to. + * @param referrer Referrer address for this deposit. + * @return shares Units of credits issued. eg imUSD or imBTC + */ + function deposit( + uint256 assets, + address receiver, + address referrer + ) external returns (uint256 shares) { + shares = _transferAndMint(assets, receiver, true); + emit Referral(referrer, receiver, assets); + } + + /** + * @notice The maximum number of vault shares that caller can mint. + * caller Account that the underlying assets will be transferred from. + * @return maxShares The maximum amount of vault shares the caller can mint. + */ + function maxMint( + address /* caller */ + ) external pure override returns (uint256 maxShares) { + maxShares = type(uint256).max; + } + + /** + * @notice Allows an on-chain or off-chain user to simulate the effects of their mint at the current block, given current on-chain conditions. + * @param shares The amount of vault shares to be minted. + * @return assets The amount of underlying assests that will be transferred from the caller. + */ + function previewMint(uint256 shares) external view override returns (uint256 assets) { + (assets, ) = _creditsToUnderlying(shares); + return assets; + } + + /** + * @notice Mint exact amount of vault shares to the receiver by transferring enough underlying asset tokens from the caller. + * @param shares The amount of vault shares to be minted. + * @param receiver The account the vault shares will be minted to. + * @return assets The amount of underlying assets that were transferred from the caller. + * Emits a {Deposit} event. + */ + function mint(uint256 shares, address receiver) external override returns (uint256 assets) { + (assets, ) = _creditsToUnderlying(shares); + _transferAndMint(assets, receiver, true); + } + + /** + * + * Returns Total number of underlying assets that caller can withdraw. + */ + function maxWithdraw(address caller) external view override returns (uint256 maxAssets) { + (maxAssets, ) = _creditsToUnderlying(balanceOf(caller)); + } + + /** + * @notice Allows an on-chain or off-chain user to simulate the effects of their withdrawal at the current block, given current on-chain conditions. + * + * Return the exact amount of Vault shares that would be redeemed by the caller if withdrawing a given exact amount of underlying assets using the withdraw method. + */ + function previewWithdraw(uint256 assets) external view override returns (uint256 shares) { + (shares, ) = _underlyingToCredits(assets); + } + + /** + * Redeems shares from owner and sends assets of underlying tokens to receiver. + * Returns Total number of underlying shares redeemed. + * Emits a {Withdraw} event. + */ + function withdraw( + uint256 assets, + address receiver, + address owner + ) external override returns (uint256 shares) { + require(assets > 0, "Must withdraw something"); + uint256 _exchangeRate; + if (automateInterestCollection) { + ISavingsManager(_savingsManager()).collectAndDistributeInterest(address(underlying)); + } + (shares, _exchangeRate) = _underlyingToCredits(assets); + + _burnTransfer(assets, shares, receiver, owner, _exchangeRate, true); + } + + /** + * @notice it must return a limited value if caller is subject to some withdrawal limit or timelock. must return balanceOf(caller) if caller is not subject to any withdrawal limit or timelock. MAY be used in the previewRedeem or redeem methods for shares input parameter. must NOT revert. + * + * Returns Total number of underlying shares that caller can redeem. + */ + function maxRedeem(address caller) external view override returns (uint256 maxShares) { + maxShares = balanceOf(caller); + } + + /** + * @notice Allows an on-chain or off-chain user to simulate the effects of their redeemption at the current block, given current on-chain conditions. + * + * Returns the exact amount of underlying assets that would be withdrawn by the caller if redeeming a given exact amount of Vault shares using the redeem method + */ + function previewRedeem(uint256 shares) external view override returns (uint256 assets) { + (assets, ) = _creditsToUnderlying(shares); + return assets; + } + + /** + * Redeems shares from owner and sends assets of underlying tokens to receiver. + * + * Returns Total number of underlying assets of underlying redeemed. + * Emits a {Withdraw} event. + */ + function redeem( + uint256 shares, + address receiver, + address owner + ) external override returns (uint256 assets) { + require(shares > 0, "Must withdraw something"); + uint256 _exchangeRate; + if (automateInterestCollection) { + ISavingsManager(_savingsManager()).collectAndDistributeInterest(address(underlying)); + } + (assets, _exchangeRate) = _creditsToUnderlying(shares); + + _burnTransfer(assets, shares, receiver, owner, _exchangeRate, true); //transferAssets=true + } + + /*/////////////////////////////////////////////////////////////// + INTERNAL DEPOSIT/MINT + //////////////////////////////////////////////////////////////*/ + function _transferAndMint( + uint256 assets, + address receiver, + bool _collectInterest + ) internal returns (uint256 shares) { + require(assets > 0, "Must deposit something"); + require(receiver != address(0), "Invalid beneficiary address"); + + // Collect recent interest generated by basket and update exchange rate + IERC20 mAsset = underlying; + if (_collectInterest) { + ISavingsManager(_savingsManager()).collectAndDistributeInterest(address(mAsset)); + } + + // Transfer tokens from sender to here + require(mAsset.transferFrom(msg.sender, address(this), assets), "Must receive tokens"); + + // Calc how many credits they receive based on currentRatio + (shares, ) = _underlyingToCredits(assets); + + // add credits to ERC20 balances + _mint(receiver, shares); + emit Deposit(msg.sender, receiver, assets, shares); + } + + /*/////////////////////////////////////////////////////////////// + INTERNAL WITHDRAW/REDEEM + //////////////////////////////////////////////////////////////*/ + + function _burnTransfer( + uint256 assets, + uint256 shares, + address receiver, + address owner, + uint256 _exchangeRate, + bool transferAssets + ) internal { + require(receiver != address(0), "Invalid beneficiary address"); + + // If caller is not the owner of the shares + uint256 allowed = allowance(owner, msg.sender); + if (msg.sender != owner && allowed != type(uint256).max) { + require(shares <= allowed, "Amount exceeds allowance"); + _approve(owner, msg.sender, allowed - shares); + } + + // Burn required shares from the owner FIRST + _burn(owner, shares); + + // Optionally, transfer tokens from here to receiver + if (transferAssets) { + require(underlying.transfer(receiver, assets), "Must send tokens"); + emit Withdraw(msg.sender, receiver, owner, assets, shares); + } + // If this withdrawal pushes the portion of stored collateral in the `connector` over a certain + // threshold (fraction + 20%), then this should trigger a _poke on the connector. This is to avoid + // a situation in which there is a rush on withdrawals for some reason, causing the connector + // balance to go up and thus having too large an exposure. + CachedData memory cachedData = _cacheData(); + ConnectorStatus memory status = _getConnectorStatus(cachedData, _exchangeRate); + if (status.inConnector > status.limit) { + _poke(cachedData, false); + } + } } diff --git a/contracts/savings/SavingsContract.sol b/contracts/savings/SavingsContract.sol index f0dc2f1e..25adf15d 100644 --- a/contracts/savings/SavingsContract.sol +++ b/contracts/savings/SavingsContract.sol @@ -15,7 +15,6 @@ import { Initializable } from "../shared/@openzeppelin-2.5/Initializable.sol"; // Libs import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; -import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import { StableMath } from "../shared/StableMath.sol"; import { YieldValidator } from "../shared/YieldValidator.sol"; @@ -26,7 +25,7 @@ import { YieldValidator } from "../shared/YieldValidator.sol"; * the value of the Savers "credits" (ERC20) relative to the amount of additional * underlying collateral that has been deposited into this contract ("interest") * @dev VERSION: 2.2 - * DATE: 2021-04-08 + * DATE: 2022-04-08 */ contract SavingsContract is ISavingsContractV3, @@ -36,7 +35,6 @@ contract SavingsContract is ImmutableModule { using StableMath for uint256; - using SafeERC20 for IERC20; // Core events for depositing and withdrawing event ExchangeRateUpdated(uint256 newExchangeRate, uint256 interestCollected); @@ -780,7 +778,9 @@ contract SavingsContract is * caller Account that the assets will be transferred from. * @return maxAssets The maximum amount of underlying assets the caller can deposit. */ - function maxDeposit(address /** caller **/) external pure override returns (uint256 maxAssets) { + function maxDeposit( + address /** caller **/ + ) external pure override returns (uint256 maxAssets) { maxAssets = type(uint256).max; } @@ -830,7 +830,9 @@ contract SavingsContract is * caller Account that the underlying assets will be transferred from. * @return maxShares The maximum amount of vault shares the caller can mint. */ - function maxMint(address /* caller */) external pure override returns (uint256 maxShares) { + function maxMint( + address /* caller */ + ) external pure override returns (uint256 maxShares) { maxShares = type(uint256).max; } @@ -973,6 +975,8 @@ contract SavingsContract is uint256 _exchangeRate, bool transferAssets ) internal { + require(receiver != address(0), "Invalid beneficiary address"); + // If caller is not the owner of the shares uint256 allowed = allowance(owner, msg.sender); if (msg.sender != owner && allowed != type(uint256).max) { diff --git a/test-fork/mUSD/aave2-migration.spec.ts b/test-fork/mUSD/aave2-migration.spec.ts index 6cf2cd11..72fd2954 100644 --- a/test-fork/mUSD/aave2-migration.spec.ts +++ b/test-fork/mUSD/aave2-migration.spec.ts @@ -1,7 +1,7 @@ import { formatUnits } from "@ethersproject/units" import { ONE_DAY } from "@utils/constants" import { impersonate } from "@utils/fork" -import { BN, simpleToExactAmount } from "@utils/math" +import { safeInfinity, simpleToExactAmount } from "@utils/math" import { increaseTime } from "@utils/time" import { expect } from "chai" import { Signer } from "ethers" @@ -52,8 +52,6 @@ const aWBtcAddress = "0x9ff58f4fFB29fA2266Ab25e75e2A8b3503311656" // Compound cTokens const cDaiAddress = "0x5d3a536E4D6DbD6114cc1Ead35777bAB948E3643" -const safeInfinity = BN.from(2).pow(256).sub(1) - context("DAI and WBTC migration to integration that can claim stkAave", () => { let governor: Signer let deployer: Signer diff --git a/test-fork/savings/sc22-mainnet-upgrade.spec.ts b/test-fork/savings/sc22-mainnet-upgrade.spec.ts index 322d7117..c356485e 100644 --- a/test-fork/savings/sc22-mainnet-upgrade.spec.ts +++ b/test-fork/savings/sc22-mainnet-upgrade.spec.ts @@ -6,11 +6,9 @@ import { deployContract } from "tasks/utils/deploy-utils" // Mainnet imBTC Vault import { BoostedSavingsVaultImbtcMainnet2__factory } from "types/generated/factories/BoostedSavingsVaultImbtcMainnet2__factory" -import { BoostedSavingsVaultImbtcMainnet2 } from "types/generated/BoostedSavingsVaultImbtcMainnet2" // Mainnet imUSD Vault import { BoostedSavingsVaultImusdMainnet2__factory } from "types/generated/factories/BoostedSavingsVaultImusdMainnet2__factory" -import { BoostedSavingsVaultImusdMainnet2 } from "types/generated/BoostedSavingsVaultImusdMainnet2" // Mainnet imBTC Contract import { SavingsContractImbtcMainnet22__factory } from "types/generated/factories/SavingsContractImbtcMainnet22__factory" @@ -22,13 +20,14 @@ import { SavingsContractImusdMainnet22 } from "types/generated/SavingsContractIm import { DelayedProxyAdmin, DelayedProxyAdmin__factory, + IERC20, ERC20__factory, IERC20__factory, Unwrapper, Unwrapper__factory, } from "types/generated" -import { assertBNClosePercent, Chain, DEAD_ADDRESS, simpleToExactAmount } from "index" +import { assertBNClosePercent, Chain, DEAD_ADDRESS, ZERO_ADDRESS, simpleToExactAmount, safeInfinity } from "index" import { BigNumber } from "@ethersproject/bignumber" import { getChainAddress, resolveAddress } from "tasks/utils/networkAddressFactory" import { upgradeContract } from "@utils/deploy" @@ -37,7 +36,7 @@ const chain = Chain.mainnet const delayedProxyAdminAddress = getChainAddress("DelayedProxyAdmin", chain) const governorAddress = getChainAddress("Governor", chain) const nexusAddress = getChainAddress("Nexus", chain) -const boostDirector = getChainAddress("BoostDirector", chain) +const unwrapperAddress = getChainAddress("Unwrapper", chain) const deployerAddress = "0x19F12C947D25Ff8a3b748829D8001cA09a28D46d" const imusdHolderAddress = "0xdA1fD36cfC50ED03ca4dd388858A78C904379fb3" @@ -52,7 +51,6 @@ const musdAddress = resolveAddress("mUSD", Chain.mainnet) const imusdAddress = resolveAddress("mUSD", Chain.mainnet, "savings") const imusdVaultAddress = resolveAddress("mUSD", Chain.mainnet, "vault") const alusdFeederPool = resolveAddress("alUSD", Chain.mainnet, "feederPool") -const mtaAddress = resolveAddress("MTA", Chain.mainnet) const mbtcAddress = resolveAddress("mBTC", Chain.mainnet) const imbtcAddress = resolveAddress("mBTC", Chain.mainnet, "savings") const imbtcVaultAddress = resolveAddress("mBTC", Chain.mainnet, "vault") @@ -61,16 +59,15 @@ const hbtcAddress = resolveAddress("HBTC", Chain.mainnet) const hbtcFeederPool = resolveAddress("HBTC", Chain.mainnet, "feederPool") // DEPLOYMENT PIPELINE -// 1. Deploy Unwrapper -// 1.1. Set the Unwrapper address as constant in imUSD Vault via initialize +// 1. Connects Unwrapper // 2. Upgrade and check storage -// 2.1. Vaults // 2.2. SavingsContracts // 3. Do some unwrapping // 3.1. Directly to unwrapper // 3.2. Via SavingsContracts // 3.3. Via SavingsVaults -context("Unwrapper and Vault upgrades", () => { +// 4. Do 4626 SavingsContracts upgrades +context("Unwrapper and Vault4626 upgrades", () => { let deployer: Signer let musdHolder: Signer let unwrapper: Unwrapper @@ -150,8 +147,8 @@ context("Unwrapper and Vault upgrades", () => { { forking: { jsonRpcUrl: process.env.NODE_URL, - // Nov-25-2021 03:15:21 PM +UTC - blockNumber: 13684204, + // Apr-01-2022 11:10:20 AM +UTC + blockNumber: 14500000, }, }, ], @@ -169,54 +166,12 @@ context("Unwrapper and Vault upgrades", () => { }) context("Stage 1", () => { - it("Deploys the unwrapper proxy contract ", async () => { - unwrapper = await deployContract(new Unwrapper__factory(deployer), "Unwrapper", [nexusAddress]) - expect(unwrapper.address).to.length(42) - - // approve tokens for router - const routers = [alusdFeederPool, hbtcFeederPool] - const tokens = [musdAddress, mbtcAddress] - await unwrapper.connect(governor).approve(routers, tokens) + it("Connects the unwrapper proxy contract ", async () => { + unwrapper = await Unwrapper__factory.connect(unwrapperAddress, deployer) }) }) context("Stage 2", () => { - describe.skip("2.1 Upgrading vaults", () => { - it("Upgrades the imUSD Vault", async () => { - const saveVaultImpl = await deployContract( - new BoostedSavingsVaultImusdMainnet2__factory(deployer), - "mStable: mUSD Savings Vault", - [], - ) - await upgradeContract( - BoostedSavingsVaultImusdMainnet2__factory as unknown as ContractFactory, - saveVaultImpl, - imusdVaultAddress, - governor, - delayedProxyAdmin, - ) - expect(await delayedProxyAdmin.getProxyImplementation(imusdVaultAddress)).eq(saveVaultImpl.address) - }) - - it("Upgrades the imBTC Vault", async () => { - const priceCoeff = simpleToExactAmount(4800, 18) - const boostCoeff = 9 - - const saveVaultImpl = await deployContract( - new BoostedSavingsVaultImbtcMainnet2__factory(deployer), - "mStable: mBTC Savings Vault", - [nexusAddress, imbtcAddress, boostDirector, priceCoeff, boostCoeff, mtaAddress], - ) - await upgradeContract( - BoostedSavingsVaultImbtcMainnet2__factory as unknown as ContractFactory, - saveVaultImpl, - imbtcVaultAddress, - governor, - delayedProxyAdmin, - ) - expect(await delayedProxyAdmin.getProxyImplementation(imbtcVaultAddress)).eq(saveVaultImpl.address) - }) - }) describe("2.2 Upgrading savings contracts", () => { it("Upgrades the imUSD contract", async () => { const musdSaveImpl = await deployContract( @@ -225,19 +180,7 @@ context("Unwrapper and Vault upgrades", () => { [], ) - // const upgradeData = musdSaveImpl.interface.encodeFunctionData("upgradeV3", [unwrapper.address]) - // Method upgradeV3 is for test purposes only - /** - solidity code - function upgradeV3(address _unwrapper) external { - // TODO - REMOVE BEFORE DEPLOYMENT - require(_unwrapper != address(0), "Invalid unwrapper address"); - unwrapper = _unwrapper; - } - */ - const upgradeData = [] - const saveContractProxy = await upgradeContract( SavingsContractImusdMainnet22__factory as unknown as ContractFactory, musdSaveImpl, @@ -247,8 +190,7 @@ context("Unwrapper and Vault upgrades", () => { upgradeData, ) - const unwrapperAddress = await saveContractProxy.unwrapper() - expect(unwrapperAddress).to.eq(unwrapper.address) + expect(await saveContractProxy.unwrapper()).to.eq(unwrapper.address) expect(await delayedProxyAdmin.getProxyImplementation(imusdAddress)).eq(musdSaveImpl.address) expect(musdAddress).eq(await musdSaveImpl.underlying()) }) @@ -273,8 +215,7 @@ context("Unwrapper and Vault upgrades", () => { delayedProxyAdmin, ) expect(await delayedProxyAdmin.getProxyImplementation(imbtcAddress)).eq(mbtcSaveImpl.address) - const unwrapperAddress = await saveContractProxy.unwrapper() - expect(unwrapperAddress).to.eq(unwrapper.address) + expect(await saveContractProxy.unwrapper()).to.eq(unwrapper.address) }) it("imBTC contract works after upgraded", async () => { @@ -502,4 +443,365 @@ context("Unwrapper and Vault upgrades", () => { }) }) }) + + context("Stage 4 Savings Contract Vault4626", () => { + const saveContracts = [ + { name: "imusd", address: imusdAddress }, + { name: "imbtc", address: imbtcAddress }, + ] + + saveContracts.forEach((sc) => { + let ctxSaveContract: SavingsContractImusdMainnet22 | SavingsContractImbtcMainnet22 + let assetAddress: string + let holderAddress: string + let anotherHolderAddress: string + let asset: IERC20 + let holder: Signer + let anotherHolder: Signer + let assetsAmount = simpleToExactAmount(10, 18) + let sharesAmount = simpleToExactAmount(100, 18) + + before(async () => { + if (sc.name === "imusd") { + holder = await impersonate(imusdHolderAddress) + anotherHolder = await impersonate(imbtcHolderAddress) + ctxSaveContract = SavingsContractImusdMainnet22__factory.connect(sc.address, holder) + assetAddress = musdAddress + assetsAmount = simpleToExactAmount(1, 18) + sharesAmount = simpleToExactAmount(10, 18) + } else { + holder = await impersonate(imbtcHolderAddress) + anotherHolder = await impersonate(imusdHolderAddress) + ctxSaveContract = SavingsContractImbtcMainnet22__factory.connect(sc.address, holder) + assetAddress = mbtcAddress + assetsAmount = simpleToExactAmount(1, 14) + sharesAmount = simpleToExactAmount(10, 14) + } + holderAddress = await holder.getAddress() + anotherHolderAddress = await anotherHolder.getAddress() + asset = IERC20__factory.connect(assetAddress, holder) + }) + describe(`SaveContract ${sc.name}`, async () => { + it("should properly store valid arguments", async () => { + expect(await ctxSaveContract.asset(), "asset").to.eq(assetAddress) + }) + describe("deposit", async () => { + it("should deposit assets to the vault", async () => { + await asset.approve(ctxSaveContract.address, simpleToExactAmount(1, 21)) + const shares = await ctxSaveContract.previewDeposit(assetsAmount) + + expect(await ctxSaveContract.maxDeposit(holderAddress), "max deposit").to.gte(assetsAmount) + expect(await ctxSaveContract.maxMint(holderAddress), "max mint").to.gte(shares) + + expect(await ctxSaveContract.maxRedeem(holderAddress), "max redeem").to.eq(0) + expect(await ctxSaveContract.maxWithdraw(holderAddress), "max withdraw").to.eq(0) + expect(await ctxSaveContract.totalAssets(), "totalAssets").to.eq(0) + expect(await ctxSaveContract.convertToShares(assetsAmount), "convertToShares").to.lte(shares) + + // Test + const tx = await ctxSaveContract.connect(holder)["deposit(uint256,address)"](assetsAmount, holderAddress) + // Verify events, storage change, balance, etc. + await expect(tx).to.emit(ctxSaveContract, "Deposit").withArgs(holderAddress, holderAddress, assetsAmount, shares) + expect(await ctxSaveContract.maxRedeem(holderAddress), "max redeem").to.lte(shares) + expect(await ctxSaveContract.maxWithdraw(holderAddress), "max withdraw").to.lte(assetsAmount) + expect(await ctxSaveContract.totalAssets(), "totalAssets").to.eq(assetsAmount) + }) + it("fails if deposits zero", async () => { + await expect(ctxSaveContract.connect(deployer)["deposit(uint256,address)"](0, holderAddress)).to.be.revertedWith( + "Must deposit something", + ) + }) + it("fails if receiver is zero", async () => { + await expect(ctxSaveContract.connect(deployer)["deposit(uint256,address)"](10, ZERO_ADDRESS)).to.be.revertedWith( + "Invalid beneficiary address", + ) + }) + }) + describe("mint", async () => { + it("should mint shares to the vault", async () => { + await asset.approve(ctxSaveContract.address, simpleToExactAmount(1, 21)) + // const shares = sharesAmount + const assets = await ctxSaveContract.previewMint(sharesAmount) + const shares = await ctxSaveContract.previewDeposit(assetsAmount) + + expect(await ctxSaveContract.maxDeposit(holderAddress), "max deposit").to.gte(assets) + expect(await ctxSaveContract.maxMint(holderAddress), "max mint").to.gte(shares) + + expect(await ctxSaveContract.maxRedeem(holderAddress), "max redeem").to.eq(0) + expect(await ctxSaveContract.maxWithdraw(holderAddress), "max withdraw").to.eq(0) + expect(await ctxSaveContract.totalAssets(), "totalAssets").to.eq(0) + + expect(await ctxSaveContract.convertToShares(assets), "convertToShares").to.lte(shares) + expect(await ctxSaveContract.convertToAssets(shares), "convertToShares").to.lte(assets) + + const tx = await ctxSaveContract.connect(holder).mint(shares, holderAddress) + // Verify events, storage change, balance, etc. + await expect(tx).to.emit(ctxSaveContract, "Deposit").withArgs(holderAddress, holderAddress, assets, shares) + + expect(await ctxSaveContract.maxRedeem(holderAddress), "max redeem").to.lte(shares) + expect(await ctxSaveContract.maxWithdraw(holderAddress), "max withdraw").to.lte(assets) + expect(await ctxSaveContract.totalAssets(), "totalAssets").to.eq(assets) + }) + it("fails if mint zero", async () => { + await expect(ctxSaveContract.connect(deployer)["mint(uint256,address)"](0, holderAddress)).to.be.revertedWith( + "Must deposit something", + ) + }) + it("fails if receiver is zero", async () => { + await expect(ctxSaveContract.connect(deployer)["mint(uint256,address)"](10, ZERO_ADDRESS)).to.be.revertedWith( + "Invalid beneficiary address", + ) + }) + }) + describe("withdraw", async () => { + it("from the vault, same caller, receiver and owner", async () => { + await asset.approve(ctxSaveContract.address, simpleToExactAmount(1, 21)) + + expect(await ctxSaveContract.maxWithdraw(holderAddress), "max withdraw").to.eq(0) + expect(await ctxSaveContract.totalAssets(), "totalAssets").to.eq(0) + + await ctxSaveContract.connect(holder)["deposit(uint256,address)"](assetsAmount, holderAddress) + expect(await ctxSaveContract.maxWithdraw(holderAddress), "max withdraw").to.gt(0) + expect(await ctxSaveContract.totalAssets(), "totalAssets").to.gt(0) + const shares = await ctxSaveContract.previewWithdraw(assetsAmount) + expect(await ctxSaveContract.maxRedeem(holderAddress), "max redeem").to.eq(shares) + + // Test + const tx = await ctxSaveContract.connect(holder).withdraw(assetsAmount, holderAddress, holderAddress) + // Verify events, storage change, balance, etc. + await expect(tx) + .to.emit(ctxSaveContract, "Withdraw") + .withArgs(holderAddress, holderAddress, holderAddress, assetsAmount, shares) + expect(await ctxSaveContract.maxRedeem(holderAddress), "max redeem").to.eq(0) + expect(await ctxSaveContract.maxWithdraw(holderAddress), "max withdraw").to.eq(0) + expect(await ctxSaveContract.totalAssets(), "totalAssets").to.eq(0) + }) + it("from the vault, caller != receiver and caller = owner", async () => { + // Alice deposits assets (owner), Alice withdraws assets (caller), Bob receives assets (receiver) + await asset.connect(holder).approve(ctxSaveContract.address, simpleToExactAmount(1, 21)) + + expect(await ctxSaveContract.maxWithdraw(holderAddress), "max withdraw").to.eq(0) + expect(await ctxSaveContract.totalAssets(), "totalAssets").to.eq(0) + + await ctxSaveContract.connect(holder)["deposit(uint256,address)"](assetsAmount, holderAddress) + expect(await ctxSaveContract.maxWithdraw(holderAddress), "max withdraw").to.gt(0) + expect(await ctxSaveContract.totalAssets(), "totalAssets").to.gt(0) + const shares = await ctxSaveContract.previewWithdraw(assetsAmount) + expect(await ctxSaveContract.maxRedeem(holderAddress), "max redeem").to.eq(shares) + + // Test + const tx = await ctxSaveContract.connect(holder).withdraw(assetsAmount, anotherHolderAddress, holderAddress) + // Verify events, storage change, balance, etc. + await expect(tx) + .to.emit(ctxSaveContract, "Withdraw") + .withArgs(holderAddress, anotherHolderAddress, holderAddress, assetsAmount, shares) + expect(await ctxSaveContract.maxRedeem(holderAddress), "max redeem").to.eq(0) + expect(await ctxSaveContract.maxWithdraw(holderAddress), "max withdraw").to.eq(0) + expect(await ctxSaveContract.totalAssets(), "totalAssets").to.eq(0) + }) + it("from the vault caller != owner, infinite approval", async () => { + // Alice deposits assets (owner), Bob withdraws assets (caller), Bob receives assets (receiver) + await asset.connect(holder).approve(ctxSaveContract.address, simpleToExactAmount(1, 21)) + await ctxSaveContract.connect(holder).approve(anotherHolderAddress, safeInfinity) + + expect(await ctxSaveContract.maxWithdraw(holderAddress), "max withdraw").to.eq(0) + expect(await ctxSaveContract.totalAssets(), "totalAssets").to.eq(0) + + await ctxSaveContract.connect(holder)["deposit(uint256,address)"](assetsAmount, holderAddress) + expect(await ctxSaveContract.maxWithdraw(holderAddress), "max withdraw").to.gt(0) + expect(await ctxSaveContract.totalAssets(), "totalAssets").to.gt(0) + const shares = await ctxSaveContract.previewWithdraw(assetsAmount) + expect(await ctxSaveContract.maxRedeem(holderAddress), "max redeem").to.eq(shares) + + // Test + const tx = await ctxSaveContract.connect(anotherHolder).withdraw(assetsAmount, anotherHolderAddress, holderAddress) + // Verify events, storage change, balance, etc. + await expect(tx) + .to.emit(ctxSaveContract, "Withdraw") + .withArgs(anotherHolderAddress, anotherHolderAddress, holderAddress, assetsAmount, shares) + expect(await ctxSaveContract.maxRedeem(holderAddress), "max redeem").to.eq(0) + expect(await ctxSaveContract.maxWithdraw(holderAddress), "max withdraw").to.eq(0) + expect(await ctxSaveContract.totalAssets(), "totalAssets").to.eq(0) + }) + it("from the vault, caller != receiver and caller != owner", async () => { + // Alice deposits assets (owner), Bob withdraws assets (caller), Bob receives assets (receiver) + await asset.connect(holder).approve(ctxSaveContract.address, simpleToExactAmount(1, 21)) + await ctxSaveContract.connect(holder).approve(anotherHolderAddress, simpleToExactAmount(1, 21)) + + expect(await ctxSaveContract.maxWithdraw(holderAddress), "max withdraw").to.eq(0) + expect(await ctxSaveContract.totalAssets(), "totalAssets").to.eq(0) + + await ctxSaveContract.connect(holder)["deposit(uint256,address)"](assetsAmount, holderAddress) + expect(await ctxSaveContract.maxWithdraw(holderAddress), "max withdraw").to.gt(0) + expect(await ctxSaveContract.totalAssets(), "totalAssets").to.gt(0) + const shares = await ctxSaveContract.previewWithdraw(assetsAmount) + expect(await ctxSaveContract.maxRedeem(holderAddress), "max redeem").to.eq(shares) + + // Test + const tx = await ctxSaveContract.connect(anotherHolder).withdraw(assetsAmount, anotherHolderAddress, holderAddress) + // Verify events, storage change, balance, etc. + await expect(tx) + .to.emit(ctxSaveContract, "Withdraw") + .withArgs(anotherHolderAddress, anotherHolderAddress, holderAddress, assetsAmount, shares) + expect(await ctxSaveContract.maxRedeem(holderAddress), "max redeem").to.eq(0) + expect(await ctxSaveContract.maxWithdraw(holderAddress), "max withdraw").to.eq(0) + expect(await ctxSaveContract.totalAssets(), "totalAssets").to.eq(0) + }) + it("fails if deposits zero", async () => { + await expect(ctxSaveContract.connect(deployer).withdraw(0, holderAddress, holderAddress)).to.be.revertedWith( + "Must withdraw something", + ) + }) + it("fails if receiver is zero", async () => { + await expect(ctxSaveContract.connect(deployer).withdraw(10, ZERO_ADDRESS, ZERO_ADDRESS)).to.be.revertedWith( + "Invalid beneficiary address", + ) + }) + it("fail if caller != owner and it has not allowance", async () => { + // Alice deposits assets (owner), Bob withdraws assets (caller), Bob receives assets (receiver) + await asset.connect(holder).approve(ctxSaveContract.address, simpleToExactAmount(1, 21)) + + expect(await ctxSaveContract.maxWithdraw(holderAddress), "max withdraw").to.eq(0) + expect(await ctxSaveContract.totalAssets(), "totalAssets").to.eq(0) + + await ctxSaveContract.connect(holder)["deposit(uint256,address)"](assetsAmount, holderAddress) + expect(await ctxSaveContract.maxWithdraw(holderAddress), "max withdraw").to.gt(0) + expect(await ctxSaveContract.totalAssets(), "totalAssets").to.gt(0) + const shares = await ctxSaveContract.previewWithdraw(assetsAmount) + expect(await ctxSaveContract.maxRedeem(holderAddress), "max redeem").to.eq(shares) + + // Test + const tx = ctxSaveContract.connect(anotherHolder).withdraw(assetsAmount, anotherHolderAddress, holderAddress) + // Verify events, storage change, balance, etc. + await expect(tx).to.be.revertedWith("Amount exceeds allowance") + }) + }) + describe("redeem", async () => { + it("from the vault, same caller, receiver and owner", async () => { + await asset.approve(ctxSaveContract.address, simpleToExactAmount(1, 21)) + + const assets = await ctxSaveContract.previewRedeem(sharesAmount) + expect(await ctxSaveContract.maxRedeem(holderAddress), "max maxRedeem").to.eq(0) + expect(await ctxSaveContract.totalAssets(), "totalAssets").to.eq(0) + + await ctxSaveContract.connect(holder)["deposit(uint256,address)"](assets, holderAddress) + expect(await ctxSaveContract.maxRedeem(holderAddress), "max maxRedeem").to.gt(0) + expect(await ctxSaveContract.totalAssets(), "totalAssets").to.gt(0) + const shares = await ctxSaveContract.maxRedeem(holderAddress) + + // Test + const tx = await ctxSaveContract + .connect(holder) + ["redeem(uint256,address,address)"](shares, holderAddress, holderAddress) + // Verify events, storage change, balance, etc. + await expect(tx) + .to.emit(ctxSaveContract, "Withdraw") + .withArgs(holderAddress, holderAddress, holderAddress, assets, shares) + expect(await ctxSaveContract.maxRedeem(holderAddress), "max redeem").to.eq(0) + expect(await ctxSaveContract.maxWithdraw(holderAddress), "max withdraw").to.eq(0) + expect(await ctxSaveContract.totalAssets(), "totalAssets").to.eq(0) + }) + it("from the vault, caller != receiver and caller = owner", async () => { + // Alice deposits assets (owner), Alice withdraws assets (caller), Bob receives assets (receiver) + await asset.connect(holder).approve(ctxSaveContract.address, simpleToExactAmount(1, 21)) + const assets = await ctxSaveContract.previewRedeem(sharesAmount) + + expect(await ctxSaveContract.maxRedeem(holderAddress), "max withdraw").to.eq(0) + expect(await ctxSaveContract.totalAssets(), "totalAssets").to.eq(0) + + await ctxSaveContract.connect(holder)["deposit(uint256,address)"](assetsAmount, holderAddress) + expect(await ctxSaveContract.maxWithdraw(holderAddress), "max withdraw").to.eq(assets) + expect(await ctxSaveContract.totalAssets(), "totalAssets").to.gt(0) + const shares = await ctxSaveContract.maxRedeem(holderAddress) + + // Test + const tx = await ctxSaveContract + .connect(holder) + ["redeem(uint256,address,address)"](shares, anotherHolderAddress, holderAddress) + // Verify events, storage change, balance, etc. + await expect(tx) + .to.emit(ctxSaveContract, "Withdraw") + .withArgs(holderAddress, anotherHolderAddress, holderAddress, assets, shares) + expect(await ctxSaveContract.maxRedeem(holderAddress), "max redeem").to.eq(0) + expect(await ctxSaveContract.maxWithdraw(holderAddress), "max withdraw").to.eq(0) + expect(await ctxSaveContract.totalAssets(), "totalAssets").to.eq(0) + }) + it("from the vault caller != owner, infinite approval", async () => { + // Alice deposits assets (owner), Bob withdraws assets (caller), Bob receives assets (receiver) + await asset.connect(holder).approve(ctxSaveContract.address, simpleToExactAmount(1, 21)) + await ctxSaveContract.connect(holder).approve(anotherHolderAddress, safeInfinity) + const assets = await ctxSaveContract.previewRedeem(sharesAmount) + + expect(await ctxSaveContract.maxWithdraw(holderAddress), "max withdraw").to.eq(0) + expect(await ctxSaveContract.totalAssets(), "totalAssets").to.eq(0) + + await ctxSaveContract.connect(holder)["deposit(uint256,address)"](assets, holderAddress) + expect(await ctxSaveContract.maxWithdraw(holderAddress), "max withdraw").to.gt(0) + expect(await ctxSaveContract.totalAssets(), "totalAssets").to.gt(0) + const shares = await ctxSaveContract.maxRedeem(holderAddress) + + // Test + const tx = await ctxSaveContract + .connect(anotherHolder) + ["redeem(uint256,address,address)"](shares, anotherHolderAddress, holderAddress) + // Verify events, storage change, balance, etc. + await expect(tx) + .to.emit(ctxSaveContract, "Withdraw") + .withArgs(anotherHolderAddress, anotherHolderAddress, holderAddress, assets, shares) + expect(await ctxSaveContract.maxRedeem(holderAddress), "max redeem").to.eq(0) + expect(await ctxSaveContract.maxWithdraw(holderAddress), "max withdraw").to.eq(0) + expect(await ctxSaveContract.totalAssets(), "totalAssets").to.eq(0) + }) + it("from the vault, caller != receiver and caller != owner", async () => { + // Alice deposits assets (owner), Bob withdraws assets (caller), Bob receives assets (receiver) + await asset.connect(holder).approve(ctxSaveContract.address, simpleToExactAmount(1, 21)) + await ctxSaveContract.connect(holder).approve(anotherHolderAddress, simpleToExactAmount(1, 21)) + + const assets = await ctxSaveContract.previewRedeem(sharesAmount) + expect(await ctxSaveContract.maxWithdraw(holderAddress), "max withdraw").to.eq(0) + expect(await ctxSaveContract.totalAssets(), "totalAssets").to.eq(0) + + await ctxSaveContract.connect(holder)["deposit(uint256,address)"](assets, holderAddress) + expect(await ctxSaveContract.maxWithdraw(holderAddress), "max withdraw").to.gt(0) + expect(await ctxSaveContract.totalAssets(), "totalAssets").to.gt(0) + const shares = await ctxSaveContract.maxRedeem(holderAddress) + + // Test + const tx = await ctxSaveContract + .connect(anotherHolder) + ["redeem(uint256,address,address)"](shares, anotherHolderAddress, holderAddress) + // Verify events, storage change, balance, etc. + await expect(tx) + .to.emit(ctxSaveContract, "Withdraw") + .withArgs(anotherHolderAddress, anotherHolderAddress, holderAddress, assets, shares) + expect(await ctxSaveContract.maxRedeem(holderAddress), "max redeem").to.eq(0) + expect(await ctxSaveContract.maxWithdraw(holderAddress), "max withdraw").to.eq(0) + expect(await ctxSaveContract.totalAssets(), "totalAssets").to.eq(0) + }) + it("fails if deposits zero", async () => { + await expect( + ctxSaveContract.connect(deployer)["redeem(uint256,address,address)"](0, holderAddress, holderAddress), + ).to.be.revertedWith("Must withdraw something") + }) + it("fails if receiver is zero", async () => { + await expect( + ctxSaveContract.connect(deployer)["redeem(uint256,address,address)"](10, ZERO_ADDRESS, ZERO_ADDRESS), + ).to.be.revertedWith("Invalid beneficiary address") + }) + it("fail if caller != owner and it has not allowance", async () => { + // Alice deposits assets (owner), Bob withdraws assets (caller), Bob receives assets (receiver) + await asset.connect(holder).approve(ctxSaveContract.address, simpleToExactAmount(1, 21)) + const assets = await ctxSaveContract.previewRedeem(sharesAmount) + await ctxSaveContract.connect(holder)["deposit(uint256,address)"](assets, holderAddress) + // Test + const tx = ctxSaveContract + .connect(anotherHolder) + ["redeem(uint256,address,address)"](sharesAmount, anotherHolderAddress, holderAddress) + // Verify events, storage change, balance, etc. + await expect(tx).to.be.revertedWith("Amount exceeds allowance") + }) + }) + }) + }) + }) }) diff --git a/test-fork/savings/sc22-polygon-upgrade.spec.ts b/test-fork/savings/sc22-polygon-upgrade.spec.ts index c304cf19..956c6c4e 100644 --- a/test-fork/savings/sc22-polygon-upgrade.spec.ts +++ b/test-fork/savings/sc22-polygon-upgrade.spec.ts @@ -5,7 +5,6 @@ import { network } from "hardhat" import { deployContract } from "tasks/utils/deploy-utils" // Polygon imUSD Vault import { StakingRewardsWithPlatformTokenImusdPolygon2__factory } from "types/generated/factories/StakingRewardsWithPlatformTokenImusdPolygon2__factory" -import { StakingRewardsWithPlatformTokenImusdPolygon2 } from "types/generated/StakingRewardsWithPlatformTokenImusdPolygon2" // Polygon imUSD Contract import { SavingsContractImusdPolygon22__factory } from "types/generated/factories/SavingsContractImusdPolygon22__factory" @@ -18,20 +17,21 @@ import { IERC20__factory, Unwrapper, Unwrapper__factory, - AssetProxy__factory, + IERC20, } from "types/generated" -import { assertBNClosePercent, Chain, DEAD_ADDRESS, simpleToExactAmount } from "index" +import { assertBNClosePercent, Chain, DEAD_ADDRESS, ZERO_ADDRESS, simpleToExactAmount, safeInfinity } from "index" import { BigNumber } from "@ethersproject/bignumber" import { getChainAddress, resolveAddress } from "tasks/utils/networkAddressFactory" import { upgradeContract } from "@utils/deploy" const chain = Chain.polygon const delayedProxyAdminAddress = getChainAddress("DelayedProxyAdmin", chain) -const multiSigAddress = "0x4aA2Dd5D5387E4b8dcf9b6Bfa4D9236038c3AD43" // 4/8 Multisig const governorAddress = resolveAddress("Governor", chain) const nexusAddress = getChainAddress("Nexus", chain) const deployerAddress = getChainAddress("OperationsSigner", chain) +const unwrapperAddress = getChainAddress("Unwrapper", chain) + const imusdHolderAddress = "0x9d8B7A637859668A903797D9f02DE2Aa05e5b0a0" const musdHolderAddress = "0xb14fFDB81E804D2792B6043B90aE5Ac973EcD53D" const vmusdHolderAddress = "0x9d8B7A637859668A903797D9f02DE2Aa05e5b0a0" @@ -42,11 +42,9 @@ const musdAddress = resolveAddress("mUSD", chain) const imusdAddress = resolveAddress("mUSD", chain, "savings") const imusdVaultAddress = resolveAddress("mUSD", chain, "vault") const fraxFeederPool = resolveAddress("FRAX", chain, "feederPool") -const mtaAddress = resolveAddress("MTA", chain) -const wmaticAddress = resolveAddress("WMATIC", chain) // DEPLOYMENT PIPELINE -// 1. Deploy Unwrapper +// 1. Connects Unwrapper // 1.1. Set the Unwrapper address as constant in imUSD Vault via initialize // 2. Upgrade and check storage // 2.1. Vaults @@ -60,7 +58,6 @@ context("Unwrapper and Vault upgrades", () => { let musdHolder: Signer let unwrapper: Unwrapper let governor: Signer - let multiSig: Signer let delayedProxyAdmin: DelayedProxyAdmin const redeemAndUnwrap = async ( @@ -128,14 +125,7 @@ context("Unwrapper and Vault upgrades", () => { expect(tokenBalanceAfter, "Token balance has increased").to.be.gt(tokenBalanceBefore) expect(holderVaultBalanceAfter, "Vault balance has decreased").to.be.lt(holderVaultBalanceBefore) } - /** - * imUSD Vault on polygon was deployed with the wrong proxy admin, this fix the issue setting the DelayedProxyAdmin as it's proxy admin - * It changes from multiSig to delayedProxyAdmin.address - */ - async function fixImusdVaultProxyAdmin() { - const imusdVaultAssetProxy = AssetProxy__factory.connect(imusdVaultAddress, multiSig) - await imusdVaultAssetProxy.changeAdmin(delayedProxyAdmin.address) - } + before("reset block number", async () => { await network.provider.request({ method: "hardhat_reset", @@ -143,7 +133,8 @@ context("Unwrapper and Vault upgrades", () => { { forking: { jsonRpcUrl: process.env.NODE_URL, - blockNumber: 24186168, + // Nov-25-2021 03:15:21 PM +UTC + blockNumber: 13684204, }, }, ], @@ -152,7 +143,6 @@ context("Unwrapper and Vault upgrades", () => { musdHolder = await impersonate(musdHolderAddress) deployer = await impersonate(deployerAddress) governor = await impersonate(governorAddress) - multiSig = await impersonate(multiSigAddress) delayedProxyAdmin = DelayedProxyAdmin__factory.connect(delayedProxyAdminAddress, governor) }) it("Test connectivity", async () => { @@ -162,43 +152,12 @@ context("Unwrapper and Vault upgrades", () => { }) context("Stage 1", () => { - it("Deploys the unwrapper proxy contract ", async () => { - unwrapper = await deployContract(new Unwrapper__factory(deployer), "Unwrapper", [nexusAddress]) - expect(unwrapper.address).to.length(42) - // approve tokens for router - const routers = [fraxFeederPool] - const tokens = [musdAddress] - await unwrapper.connect(governor).approve(routers, tokens) + it("Connects the unwrapper proxy contract ", async () => { + unwrapper = await Unwrapper__factory.connect(unwrapperAddress, deployer) }) }) context("Stage 2", () => { - describe.skip("2.1 Upgrading vaults", () => { - it("Upgrades the imUSD Vault", async () => { - await fixImusdVaultProxyAdmin() - - const constructorArguments = [ - nexusAddress, // 0x3c6fbb8cbfcb75ecec5128e9f73307f2cb33f2f6 deployed - imusdAddress, // imUSD - mtaAddress, // MTA bridged to Polygon - wmaticAddress, // Wrapped Matic on Polygon - ] - - const saveVaultImpl = await deployContract( - new StakingRewardsWithPlatformTokenImusdPolygon2__factory(deployer), - "mStable: mUSD Savings Vault", - constructorArguments, - ) - await upgradeContract( - StakingRewardsWithPlatformTokenImusdPolygon2__factory as unknown as ContractFactory, - saveVaultImpl, - imusdVaultAddress, - governor, - delayedProxyAdmin, - ) - expect(await delayedProxyAdmin.getProxyImplementation(imusdVaultAddress)).eq(saveVaultImpl.address) - }) - }) describe("2.2 Upgrading savings contracts", () => { it("Upgrades the imUSD contract", async () => { const constructorArguments = [nexusAddress, musdAddress, unwrapper.address] @@ -215,8 +174,7 @@ context("Unwrapper and Vault upgrades", () => { delayedProxyAdmin, ) - const unwrapperAddress = await saveContractProxy.unwrapper() - expect(unwrapperAddress).to.eq(unwrapper.address) + expect(await saveContractProxy.unwrapper()).to.eq(unwrapper.address) expect(await delayedProxyAdmin.getProxyImplementation(imusdAddress)).eq(musdSaveImpl.address) expect(musdAddress).eq(await musdSaveImpl.underlying()) }) @@ -424,4 +382,356 @@ context("Unwrapper and Vault upgrades", () => { }) }) }) + context("Stage 4 Savings Contract Vault4626", () => { + const saveContracts = [{ name: "imusd", address: imusdAddress }] + + saveContracts.forEach((sc) => { + let ctxSaveContract: SavingsContractImusdPolygon22 + let assetAddress: string + let holderAddress: string + let anotherHolderAddress: string + let asset: IERC20 + let holder: Signer + let anotherHolder: Signer + let assetsAmount = simpleToExactAmount(10, 18) + let sharesAmount = simpleToExactAmount(100, 18) + + before(async () => { + if (sc.name === "imusd") { + holder = await impersonate(imusdHolderAddress) + anotherHolder = await impersonate(musdHolderAddress) + ctxSaveContract = SavingsContractImusdPolygon22__factory.connect(sc.address, holder) + assetAddress = musdAddress + assetsAmount = simpleToExactAmount(1, 18) + sharesAmount = simpleToExactAmount(10, 18) + } else { + // not needed now. + } + holderAddress = await holder.getAddress() + anotherHolderAddress = await anotherHolder.getAddress() + asset = IERC20__factory.connect(assetAddress, holder) + }) + describe(`SaveContract ${sc.name}`, async () => { + it("should properly store valid arguments", async () => { + expect(await ctxSaveContract.asset(), "asset").to.eq(assetAddress) + }) + describe("deposit", async () => { + it("should deposit assets to the vault", async () => { + await asset.approve(ctxSaveContract.address, simpleToExactAmount(1, 21)) + const shares = await ctxSaveContract.previewDeposit(assetsAmount) + + expect(await ctxSaveContract.maxDeposit(holderAddress), "max deposit").to.gte(assetsAmount) + expect(await ctxSaveContract.maxMint(holderAddress), "max mint").to.gte(shares) + + expect(await ctxSaveContract.maxRedeem(holderAddress), "max redeem").to.eq(0) + expect(await ctxSaveContract.maxWithdraw(holderAddress), "max withdraw").to.eq(0) + expect(await ctxSaveContract.totalAssets(), "totalAssets").to.eq(0) + expect(await ctxSaveContract.convertToShares(assetsAmount), "convertToShares").to.lte(shares) + + // Test + const tx = await ctxSaveContract.connect(holder)["deposit(uint256,address)"](assetsAmount, holderAddress) + // Verify events, storage change, balance, etc. + await expect(tx).to.emit(ctxSaveContract, "Deposit").withArgs(holderAddress, holderAddress, assetsAmount, shares) + expect(await ctxSaveContract.maxRedeem(holderAddress), "max redeem").to.lte(shares) + expect(await ctxSaveContract.maxWithdraw(holderAddress), "max withdraw").to.lte(assetsAmount) + expect(await ctxSaveContract.totalAssets(), "totalAssets").to.eq(assetsAmount) + }) + it("fails if deposits zero", async () => { + await expect(ctxSaveContract.connect(deployer)["deposit(uint256,address)"](0, holderAddress)).to.be.revertedWith( + "Must deposit something", + ) + }) + it("fails if receiver is zero", async () => { + await expect(ctxSaveContract.connect(deployer)["deposit(uint256,address)"](10, ZERO_ADDRESS)).to.be.revertedWith( + "Invalid beneficiary address", + ) + }) + }) + describe("mint", async () => { + it("should mint shares to the vault", async () => { + await asset.approve(ctxSaveContract.address, simpleToExactAmount(1, 21)) + // const shares = sharesAmount + const assets = await ctxSaveContract.previewMint(sharesAmount) + const shares = await ctxSaveContract.previewDeposit(assetsAmount) + + expect(await ctxSaveContract.maxDeposit(holderAddress), "max deposit").to.gte(assets) + expect(await ctxSaveContract.maxMint(holderAddress), "max mint").to.gte(shares) + + expect(await ctxSaveContract.maxRedeem(holderAddress), "max redeem").to.eq(0) + expect(await ctxSaveContract.maxWithdraw(holderAddress), "max withdraw").to.eq(0) + expect(await ctxSaveContract.totalAssets(), "totalAssets").to.eq(0) + + expect(await ctxSaveContract.convertToShares(assets), "convertToShares").to.lte(shares) + expect(await ctxSaveContract.convertToAssets(shares), "convertToShares").to.lte(assets) + + const tx = await ctxSaveContract.connect(holder).mint(shares, holderAddress) + // Verify events, storage change, balance, etc. + await expect(tx).to.emit(ctxSaveContract, "Deposit").withArgs(holderAddress, holderAddress, assets, shares) + + expect(await ctxSaveContract.maxRedeem(holderAddress), "max redeem").to.lte(shares) + expect(await ctxSaveContract.maxWithdraw(holderAddress), "max withdraw").to.lte(assets) + expect(await ctxSaveContract.totalAssets(), "totalAssets").to.eq(assets) + }) + it("fails if mint zero", async () => { + await expect(ctxSaveContract.connect(deployer)["mint(uint256,address)"](0, holderAddress)).to.be.revertedWith( + "Must deposit something", + ) + }) + it("fails if receiver is zero", async () => { + await expect(ctxSaveContract.connect(deployer)["mint(uint256,address)"](10, ZERO_ADDRESS)).to.be.revertedWith( + "Invalid beneficiary address", + ) + }) + }) + describe("withdraw", async () => { + it("from the vault, same caller, receiver and owner", async () => { + await asset.approve(ctxSaveContract.address, simpleToExactAmount(1, 21)) + + expect(await ctxSaveContract.maxWithdraw(holderAddress), "max withdraw").to.eq(0) + expect(await ctxSaveContract.totalAssets(), "totalAssets").to.eq(0) + + await ctxSaveContract.connect(holder)["deposit(uint256,address)"](assetsAmount, holderAddress) + expect(await ctxSaveContract.maxWithdraw(holderAddress), "max withdraw").to.gt(0) + expect(await ctxSaveContract.totalAssets(), "totalAssets").to.gt(0) + const shares = await ctxSaveContract.previewWithdraw(assetsAmount) + expect(await ctxSaveContract.maxRedeem(holderAddress), "max redeem").to.eq(shares) + + // Test + const tx = await ctxSaveContract.connect(holder).withdraw(assetsAmount, holderAddress, holderAddress) + // Verify events, storage change, balance, etc. + await expect(tx) + .to.emit(ctxSaveContract, "Withdraw") + .withArgs(holderAddress, holderAddress, holderAddress, assetsAmount, shares) + expect(await ctxSaveContract.maxRedeem(holderAddress), "max redeem").to.eq(0) + expect(await ctxSaveContract.maxWithdraw(holderAddress), "max withdraw").to.eq(0) + expect(await ctxSaveContract.totalAssets(), "totalAssets").to.eq(0) + }) + it("from the vault, caller != receiver and caller = owner", async () => { + // Alice deposits assets (owner), Alice withdraws assets (caller), Bob receives assets (receiver) + await asset.connect(holder).approve(ctxSaveContract.address, simpleToExactAmount(1, 21)) + + expect(await ctxSaveContract.maxWithdraw(holderAddress), "max withdraw").to.eq(0) + expect(await ctxSaveContract.totalAssets(), "totalAssets").to.eq(0) + + await ctxSaveContract.connect(holder)["deposit(uint256,address)"](assetsAmount, holderAddress) + expect(await ctxSaveContract.maxWithdraw(holderAddress), "max withdraw").to.gt(0) + expect(await ctxSaveContract.totalAssets(), "totalAssets").to.gt(0) + const shares = await ctxSaveContract.previewWithdraw(assetsAmount) + expect(await ctxSaveContract.maxRedeem(holderAddress), "max redeem").to.eq(shares) + + // Test + const tx = await ctxSaveContract.connect(holder).withdraw(assetsAmount, anotherHolderAddress, holderAddress) + // Verify events, storage change, balance, etc. + await expect(tx) + .to.emit(ctxSaveContract, "Withdraw") + .withArgs(holderAddress, anotherHolderAddress, holderAddress, assetsAmount, shares) + expect(await ctxSaveContract.maxRedeem(holderAddress), "max redeem").to.eq(0) + expect(await ctxSaveContract.maxWithdraw(holderAddress), "max withdraw").to.eq(0) + expect(await ctxSaveContract.totalAssets(), "totalAssets").to.eq(0) + }) + it("from the vault caller != owner, infinite approval", async () => { + // Alice deposits assets (owner), Bob withdraws assets (caller), Bob receives assets (receiver) + await asset.connect(holder).approve(ctxSaveContract.address, simpleToExactAmount(1, 21)) + await ctxSaveContract.connect(holder).approve(anotherHolderAddress, safeInfinity) + + expect(await ctxSaveContract.maxWithdraw(holderAddress), "max withdraw").to.eq(0) + expect(await ctxSaveContract.totalAssets(), "totalAssets").to.eq(0) + + await ctxSaveContract.connect(holder)["deposit(uint256,address)"](assetsAmount, holderAddress) + expect(await ctxSaveContract.maxWithdraw(holderAddress), "max withdraw").to.gt(0) + expect(await ctxSaveContract.totalAssets(), "totalAssets").to.gt(0) + const shares = await ctxSaveContract.previewWithdraw(assetsAmount) + expect(await ctxSaveContract.maxRedeem(holderAddress), "max redeem").to.eq(shares) + + // Test + const tx = await ctxSaveContract.connect(anotherHolder).withdraw(assetsAmount, anotherHolderAddress, holderAddress) + // Verify events, storage change, balance, etc. + await expect(tx) + .to.emit(ctxSaveContract, "Withdraw") + .withArgs(anotherHolderAddress, anotherHolderAddress, holderAddress, assetsAmount, shares) + expect(await ctxSaveContract.maxRedeem(holderAddress), "max redeem").to.eq(0) + expect(await ctxSaveContract.maxWithdraw(holderAddress), "max withdraw").to.eq(0) + expect(await ctxSaveContract.totalAssets(), "totalAssets").to.eq(0) + }) + it("from the vault, caller != receiver and caller != owner", async () => { + // Alice deposits assets (owner), Bob withdraws assets (caller), Bob receives assets (receiver) + await asset.connect(holder).approve(ctxSaveContract.address, simpleToExactAmount(1, 21)) + await ctxSaveContract.connect(holder).approve(anotherHolderAddress, simpleToExactAmount(1, 21)) + + expect(await ctxSaveContract.maxWithdraw(holderAddress), "max withdraw").to.eq(0) + expect(await ctxSaveContract.totalAssets(), "totalAssets").to.eq(0) + + await ctxSaveContract.connect(holder)["deposit(uint256,address)"](assetsAmount, holderAddress) + expect(await ctxSaveContract.maxWithdraw(holderAddress), "max withdraw").to.gt(0) + expect(await ctxSaveContract.totalAssets(), "totalAssets").to.gt(0) + const shares = await ctxSaveContract.previewWithdraw(assetsAmount) + expect(await ctxSaveContract.maxRedeem(holderAddress), "max redeem").to.eq(shares) + + // Test + const tx = await ctxSaveContract.connect(anotherHolder).withdraw(assetsAmount, anotherHolderAddress, holderAddress) + // Verify events, storage change, balance, etc. + await expect(tx) + .to.emit(ctxSaveContract, "Withdraw") + .withArgs(anotherHolderAddress, anotherHolderAddress, holderAddress, assetsAmount, shares) + expect(await ctxSaveContract.maxRedeem(holderAddress), "max redeem").to.eq(0) + expect(await ctxSaveContract.maxWithdraw(holderAddress), "max withdraw").to.eq(0) + expect(await ctxSaveContract.totalAssets(), "totalAssets").to.eq(0) + }) + it("fails if deposits zero", async () => { + await expect(ctxSaveContract.connect(deployer).withdraw(0, holderAddress, holderAddress)).to.be.revertedWith( + "Must withdraw something", + ) + }) + it("fails if receiver is zero", async () => { + await expect(ctxSaveContract.connect(deployer).withdraw(10, ZERO_ADDRESS, ZERO_ADDRESS)).to.be.revertedWith( + "Invalid beneficiary address", + ) + }) + it("fail if caller != owner and it has not allowance", async () => { + // Alice deposits assets (owner), Bob withdraws assets (caller), Bob receives assets (receiver) + await asset.connect(holder).approve(ctxSaveContract.address, simpleToExactAmount(1, 21)) + + expect(await ctxSaveContract.maxWithdraw(holderAddress), "max withdraw").to.eq(0) + expect(await ctxSaveContract.totalAssets(), "totalAssets").to.eq(0) + + await ctxSaveContract.connect(holder)["deposit(uint256,address)"](assetsAmount, holderAddress) + expect(await ctxSaveContract.maxWithdraw(holderAddress), "max withdraw").to.gt(0) + expect(await ctxSaveContract.totalAssets(), "totalAssets").to.gt(0) + const shares = await ctxSaveContract.previewWithdraw(assetsAmount) + expect(await ctxSaveContract.maxRedeem(holderAddress), "max redeem").to.eq(shares) + + // Test + const tx = ctxSaveContract.connect(anotherHolder).withdraw(assetsAmount, anotherHolderAddress, holderAddress) + // Verify events, storage change, balance, etc. + await expect(tx).to.be.revertedWith("Amount exceeds allowance") + }) + }) + describe("redeem", async () => { + it("from the vault, same caller, receiver and owner", async () => { + await asset.approve(ctxSaveContract.address, simpleToExactAmount(1, 21)) + + const assets = await ctxSaveContract.previewRedeem(sharesAmount) + expect(await ctxSaveContract.maxRedeem(holderAddress), "max maxRedeem").to.eq(0) + expect(await ctxSaveContract.totalAssets(), "totalAssets").to.eq(0) + + await ctxSaveContract.connect(holder)["deposit(uint256,address)"](assets, holderAddress) + expect(await ctxSaveContract.maxRedeem(holderAddress), "max maxRedeem").to.gt(0) + expect(await ctxSaveContract.totalAssets(), "totalAssets").to.gt(0) + const shares = await ctxSaveContract.maxRedeem(holderAddress) + + // Test + const tx = await ctxSaveContract + .connect(holder) + ["redeem(uint256,address,address)"](shares, holderAddress, holderAddress) + // Verify events, storage change, balance, etc. + await expect(tx) + .to.emit(ctxSaveContract, "Withdraw") + .withArgs(holderAddress, holderAddress, holderAddress, assets, shares) + expect(await ctxSaveContract.maxRedeem(holderAddress), "max redeem").to.eq(0) + expect(await ctxSaveContract.maxWithdraw(holderAddress), "max withdraw").to.eq(0) + expect(await ctxSaveContract.totalAssets(), "totalAssets").to.eq(0) + }) + it("from the vault, caller != receiver and caller = owner", async () => { + // Alice deposits assets (owner), Alice withdraws assets (caller), Bob receives assets (receiver) + await asset.connect(holder).approve(ctxSaveContract.address, simpleToExactAmount(1, 21)) + const assets = await ctxSaveContract.previewRedeem(sharesAmount) + + expect(await ctxSaveContract.maxRedeem(holderAddress), "max withdraw").to.eq(0) + expect(await ctxSaveContract.totalAssets(), "totalAssets").to.eq(0) + + await ctxSaveContract.connect(holder)["deposit(uint256,address)"](assetsAmount, holderAddress) + expect(await ctxSaveContract.maxWithdraw(holderAddress), "max withdraw").to.eq(assets) + expect(await ctxSaveContract.totalAssets(), "totalAssets").to.gt(0) + const shares = await ctxSaveContract.maxRedeem(holderAddress) + + // Test + const tx = await ctxSaveContract + .connect(holder) + ["redeem(uint256,address,address)"](shares, anotherHolderAddress, holderAddress) + // Verify events, storage change, balance, etc. + await expect(tx) + .to.emit(ctxSaveContract, "Withdraw") + .withArgs(holderAddress, anotherHolderAddress, holderAddress, assets, shares) + expect(await ctxSaveContract.maxRedeem(holderAddress), "max redeem").to.eq(0) + expect(await ctxSaveContract.maxWithdraw(holderAddress), "max withdraw").to.eq(0) + expect(await ctxSaveContract.totalAssets(), "totalAssets").to.eq(0) + }) + it("from the vault caller != owner, infinite approval", async () => { + // Alice deposits assets (owner), Bob withdraws assets (caller), Bob receives assets (receiver) + await asset.connect(holder).approve(ctxSaveContract.address, simpleToExactAmount(1, 21)) + await ctxSaveContract.connect(holder).approve(anotherHolderAddress, safeInfinity) + const assets = await ctxSaveContract.previewRedeem(sharesAmount) + + expect(await ctxSaveContract.maxWithdraw(holderAddress), "max withdraw").to.eq(0) + expect(await ctxSaveContract.totalAssets(), "totalAssets").to.eq(0) + + await ctxSaveContract.connect(holder)["deposit(uint256,address)"](assets, holderAddress) + expect(await ctxSaveContract.maxWithdraw(holderAddress), "max withdraw").to.gt(0) + expect(await ctxSaveContract.totalAssets(), "totalAssets").to.gt(0) + const shares = await ctxSaveContract.maxRedeem(holderAddress) + + // Test + const tx = await ctxSaveContract + .connect(anotherHolder) + ["redeem(uint256,address,address)"](shares, anotherHolderAddress, holderAddress) + // Verify events, storage change, balance, etc. + await expect(tx) + .to.emit(ctxSaveContract, "Withdraw") + .withArgs(anotherHolderAddress, anotherHolderAddress, holderAddress, assets, shares) + expect(await ctxSaveContract.maxRedeem(holderAddress), "max redeem").to.eq(0) + expect(await ctxSaveContract.maxWithdraw(holderAddress), "max withdraw").to.eq(0) + expect(await ctxSaveContract.totalAssets(), "totalAssets").to.eq(0) + }) + it("from the vault, caller != receiver and caller != owner", async () => { + // Alice deposits assets (owner), Bob withdraws assets (caller), Bob receives assets (receiver) + await asset.connect(holder).approve(ctxSaveContract.address, simpleToExactAmount(1, 21)) + await ctxSaveContract.connect(holder).approve(anotherHolderAddress, simpleToExactAmount(1, 21)) + + const assets = await ctxSaveContract.previewRedeem(sharesAmount) + expect(await ctxSaveContract.maxWithdraw(holderAddress), "max withdraw").to.eq(0) + expect(await ctxSaveContract.totalAssets(), "totalAssets").to.eq(0) + + await ctxSaveContract.connect(holder)["deposit(uint256,address)"](assets, holderAddress) + expect(await ctxSaveContract.maxWithdraw(holderAddress), "max withdraw").to.gt(0) + expect(await ctxSaveContract.totalAssets(), "totalAssets").to.gt(0) + const shares = await ctxSaveContract.maxRedeem(holderAddress) + + // Test + const tx = await ctxSaveContract + .connect(anotherHolder) + ["redeem(uint256,address,address)"](shares, anotherHolderAddress, holderAddress) + // Verify events, storage change, balance, etc. + await expect(tx) + .to.emit(ctxSaveContract, "Withdraw") + .withArgs(anotherHolderAddress, anotherHolderAddress, holderAddress, assets, shares) + expect(await ctxSaveContract.maxRedeem(holderAddress), "max redeem").to.eq(0) + expect(await ctxSaveContract.maxWithdraw(holderAddress), "max withdraw").to.eq(0) + expect(await ctxSaveContract.totalAssets(), "totalAssets").to.eq(0) + }) + it("fails if deposits zero", async () => { + await expect( + ctxSaveContract.connect(deployer)["redeem(uint256,address,address)"](0, holderAddress, holderAddress), + ).to.be.revertedWith("Must withdraw something") + }) + it("fails if receiver is zero", async () => { + await expect( + ctxSaveContract.connect(deployer)["redeem(uint256,address,address)"](10, ZERO_ADDRESS, ZERO_ADDRESS), + ).to.be.revertedWith("Invalid beneficiary address") + }) + it("fail if caller != owner and it has not allowance", async () => { + // Alice deposits assets (owner), Bob withdraws assets (caller), Bob receives assets (receiver) + await asset.connect(holder).approve(ctxSaveContract.address, simpleToExactAmount(1, 21)) + const assets = await ctxSaveContract.previewRedeem(sharesAmount) + await ctxSaveContract.connect(holder)["deposit(uint256,address)"](assets, holderAddress) + // Test + const tx = ctxSaveContract + .connect(anotherHolder) + ["redeem(uint256,address,address)"](sharesAmount, anotherHolderAddress, holderAddress) + // Verify events, storage change, balance, etc. + await expect(tx).to.be.revertedWith("Amount exceeds allowance") + }) + }) + }) + }) + }) }) diff --git a/test-utils/math.ts b/test-utils/math.ts index 4e44cf81..274d44c3 100644 --- a/test-utils/math.ts +++ b/test-utils/math.ts @@ -107,3 +107,5 @@ export const sqrt = (value: BN | number): BN => { } // Returns the sum of two big numbers export const sum = (a: BN, b: BN): BN => a.add(b) + +export const safeInfinity = BN.from(2).pow(256).sub(1) diff --git a/test/shared/ERC4626.behaviour.ts b/test/shared/ERC4626.behaviour.ts index cf521f51..a4def75b 100644 --- a/test/shared/ERC4626.behaviour.ts +++ b/test/shared/ERC4626.behaviour.ts @@ -61,7 +61,6 @@ export function shouldBehaveLikeERC4626(ctx: IERC4626BehaviourContext): void { ) }) }) - describe("mint", async () => { it("should mint shares to the vault", async () => { await ctx.asset.approve(ctx.vault.address, simpleToExactAmount(1, 21)) @@ -98,7 +97,6 @@ export function shouldBehaveLikeERC4626(ctx: IERC4626BehaviourContext): void { ) }) }) - describe("withdraw", async () => { it("from the vault, same caller, receiver and owner", async () => { await ctx.asset.approve(ctx.vault.address, simpleToExactAmount(1, 21)) From df73caf7356eef0b9e40a6bec32bba48d16f70c8 Mon Sep 17 00:00:00 2001 From: doncesarts Date: Wed, 13 Apr 2022 15:21:06 +0100 Subject: [PATCH 09/14] Merge branch 'master' into feature/IERC4626Vault --- test/shared/ERC4626.behaviour.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/test/shared/ERC4626.behaviour.ts b/test/shared/ERC4626.behaviour.ts index a4def75b..f2c87f5c 100644 --- a/test/shared/ERC4626.behaviour.ts +++ b/test/shared/ERC4626.behaviour.ts @@ -1,11 +1,10 @@ import { ZERO_ADDRESS } from "@utils/constants" import { MassetDetails, MassetMachine, StandardAccounts } from "@utils/machines" -import { BN, simpleToExactAmount } from "@utils/math" +import { BN, simpleToExactAmount, safeInfinity } from "@utils/math" import { expect } from "chai" import { Account } from "types" import { ERC20, ERC205, IERC20Metadata, IERC4626Vault } from "types/generated" -const safeInfinity = BN.from(2).pow(96).sub(1) export interface IERC4626BehaviourContext { vault: IERC4626Vault From 507017c8b41b111e15f491f9c02d228e63a3f7c8 Mon Sep 17 00:00:00 2001 From: doncesarts Date: Wed, 20 Apr 2022 13:27:47 +0100 Subject: [PATCH 10/14] test: finish fork tests for IERC4626Vault --- .../legacy-upgraded/imbtc-mainnet-22.sol | 20 +- .../legacy-upgraded/imusd-mainnet-21.sol | 1859 +++++++++++++++++ .../legacy-upgraded/imusd-mainnet-22.sol | 29 +- .../legacy-upgraded/imusd-polygon-22.sol | 20 +- contracts/savings/SavingsContract.sol | 20 +- tasks/deploySavingsContract4626.ts | 146 ++ .../savings/sc22-mainnet-upgrade.spec.ts | 584 +++--- .../savings/sc22-polygon-upgrade.spec.ts | 541 ++--- test-utils/fork.ts | 62 +- test/savings/savings-contract.spec.ts | 45 +- test/shared/ERC4626.behaviour.ts | 3 +- 11 files changed, 2651 insertions(+), 678 deletions(-) create mode 100644 contracts/legacy-upgraded/imusd-mainnet-21.sol create mode 100644 tasks/deploySavingsContract4626.ts diff --git a/contracts/legacy-upgraded/imbtc-mainnet-22.sol b/contracts/legacy-upgraded/imbtc-mainnet-22.sol index c8932275..a44a75d7 100644 --- a/contracts/legacy-upgraded/imbtc-mainnet-22.sol +++ b/contracts/legacy-upgraded/imbtc-mainnet-22.sol @@ -1322,7 +1322,6 @@ contract SavingsContract_imbtc_mainnet_22 is bool _collectInterest ) internal returns (uint256 creditsIssued) { creditsIssued = _transferAndMint(_underlying, _beneficiary, _collectInterest); - emit SavingsDeposited(_beneficiary, _underlying, creditsIssued); } /*************************************** @@ -1925,6 +1924,24 @@ contract SavingsContract_imbtc_mainnet_22 is _transferAndMint(assets, receiver, true); } + /** + * @notice Mint exact amount of vault shares to the receiver by transferring enough underlying asset tokens from the caller. + * @param shares The amount of vault shares to be minted. + * @param receiver The account the vault shares will be minted to. + * @param referrer Referrer address for this deposit. + * @return assets The amount of underlying assets that were transferred from the caller. + * Emits a {Deposit}, {Referral} events + */ + function mint( + uint256 shares, + address receiver, + address referrer + ) external returns (uint256 assets) { + (assets, ) = _creditsToUnderlying(shares); + _transferAndMint(assets, receiver, true); + emit Referral(referrer, receiver, assets); + } + /** * * Returns Total number of underlying assets that caller can withdraw. @@ -2028,6 +2045,7 @@ contract SavingsContract_imbtc_mainnet_22 is // add credits to ERC20 balances _mint(receiver, shares); emit Deposit(msg.sender, receiver, assets, shares); + emit SavingsDeposited(receiver, assets, shares); } /*/////////////////////////////////////////////////////////////// diff --git a/contracts/legacy-upgraded/imusd-mainnet-21.sol b/contracts/legacy-upgraded/imusd-mainnet-21.sol new file mode 100644 index 00000000..42b20155 --- /dev/null +++ b/contracts/legacy-upgraded/imusd-mainnet-21.sol @@ -0,0 +1,1859 @@ +pragma solidity 0.5.16; + +interface IUnwrapper { + // @dev Get bAssetOut status + function getIsBassetOut( + address _masset, + bool _inputIsCredit, + address _output + ) external view returns (bool isBassetOut); + + /// @dev Estimate output + function getUnwrapOutput( + bool _isBassetOut, + address _router, + address _input, + bool _inputIsCredit, + address _output, + uint256 _amount + ) external view returns (uint256 output); + + /// @dev Unwrap and send + function unwrapAndSend( + bool _isBassetOut, + address _router, + address _input, + address _output, + uint256 _amount, + uint256 _minAmountOut, + address _beneficiary + ) external returns (uint256 outputQuantity); +} + +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; +} + +interface ISavingsContractV1 { + function depositInterest(uint256 _amount) external; + + function depositSavings(uint256 _amount) external returns (uint256 creditsIssued); + + function redeem(uint256 _amount) external returns (uint256 massetReturned); + + function exchangeRate() external view returns (uint256); + + function creditBalances(address) external view returns (uint256); +} + +interface ISavingsContractV3 { + // 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 balance); // V2 + + function underlyingToCredits(uint256 _credits) external view returns (uint256 underlying); // V2 + + function creditsToUnderlying(uint256 _underlying) external view returns (uint256 credits); // V2 + + // -------------------------------------------- + + function redeemAndUnwrap( + uint256 _amount, + bool _isCreditAmt, + uint256 _minAmountOut, + address _output, + address _beneficiary, + address _router, + bool _isBassetOut + ) + external + returns ( + uint256 creditsBurned, + uint256 massetRedeemed, + uint256 outputQuantity + ); + + function depositSavings( + uint256 _underlying, + address _beneficiary, + address _referrer + ) external returns (uint256 creditsIssued); +} + +/* + * @dev Provides information about the current execution context, including the + * sender of the transaction and its data. While these are generally available + * via msg.sender and msg.data, they should not be accessed in such a direct + * manner, since when dealing with GSN meta-transactions the account sending and + * paying for execution may not be the actual sender (as far as an application + * is concerned). + * + * This contract is only required for intermediate, library-like contracts. + */ +contract Context { + // Empty internal constructor, to prevent people from mistakenly deploying + // an instance of this contract, which should be used via inheritance. + constructor() internal {} + + // solhint-disable-previous-line no-empty-blocks + + function _msgSender() internal view returns (address payable) { + return msg.sender; + } + + function _msgData() internal view returns (bytes memory) { + this; // silence state mutability warning without generating bytecode - see https://github.com/ethereum/solidity/issues/2691 + return msg.data; + } +} + +/** + * @dev Interface of the ERC20 standard as defined in the EIP. Does not include + * the optional functions; to access them see {ERC20Detailed}. + */ +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); +} + +/** + * @dev Wrappers over Solidity's arithmetic operations with added overflow + * checks. + * + * Arithmetic operations in Solidity wrap on overflow. This can easily result + * in bugs, because programmers usually assume that an overflow raises an + * error, which is the standard behavior in high level programming languages. + * `SafeMath` restores this intuition by reverting the transaction when an + * operation overflows. + * + * Using this library instead of the unchecked operations eliminates an entire + * class of bugs, so it's recommended to use it always. + */ +library SafeMath { + /** + * @dev Returns the addition of two unsigned integers, reverting on + * overflow. + * + * Counterpart to Solidity's `+` operator. + * + * Requirements: + * - Addition cannot overflow. + */ + function add(uint256 a, uint256 b) internal pure returns (uint256) { + uint256 c = a + b; + require(c >= a, "SafeMath: addition overflow"); + + return c; + } + + /** + * @dev Returns the subtraction of two unsigned integers, reverting on + * overflow (when the result is negative). + * + * Counterpart to Solidity's `-` operator. + * + * Requirements: + * - Subtraction cannot overflow. + */ + function sub(uint256 a, uint256 b) internal pure returns (uint256) { + return sub(a, b, "SafeMath: subtraction overflow"); + } + + /** + * @dev Returns the subtraction of two unsigned integers, reverting with custom message on + * overflow (when the result is negative). + * + * Counterpart to Solidity's `-` operator. + * + * Requirements: + * - Subtraction cannot overflow. + * + * _Available since v2.4.0._ + */ + function sub( + uint256 a, + uint256 b, + string memory errorMessage + ) internal pure returns (uint256) { + require(b <= a, errorMessage); + uint256 c = a - b; + + return c; + } + + /** + * @dev Returns the multiplication of two unsigned integers, reverting on + * overflow. + * + * Counterpart to Solidity's `*` operator. + * + * Requirements: + * - Multiplication cannot overflow. + */ + function mul(uint256 a, uint256 b) internal pure returns (uint256) { + // Gas optimization: this is cheaper than requiring 'a' not being zero, but the + // benefit is lost if 'b' is also tested. + // See: https://github.com/OpenZeppelin/openzeppelin-contracts/pull/522 + if (a == 0) { + return 0; + } + + uint256 c = a * b; + require(c / a == b, "SafeMath: multiplication overflow"); + + return c; + } + + /** + * @dev Returns the integer division of two unsigned integers. Reverts on + * division by zero. The result is rounded towards zero. + * + * Counterpart to Solidity's `/` operator. Note: this function uses a + * `revert` opcode (which leaves remaining gas untouched) while Solidity + * uses an invalid opcode to revert (consuming all remaining gas). + * + * Requirements: + * - The divisor cannot be zero. + */ + function div(uint256 a, uint256 b) internal pure returns (uint256) { + return div(a, b, "SafeMath: division by zero"); + } + + /** + * @dev Returns the integer division of two unsigned integers. Reverts with custom message on + * division by zero. The result is rounded towards zero. + * + * Counterpart to Solidity's `/` operator. Note: this function uses a + * `revert` opcode (which leaves remaining gas untouched) while Solidity + * uses an invalid opcode to revert (consuming all remaining gas). + * + * Requirements: + * - The divisor cannot be zero. + * + * _Available since v2.4.0._ + */ + function div( + uint256 a, + uint256 b, + string memory errorMessage + ) internal pure returns (uint256) { + // Solidity only automatically asserts when dividing by 0 + require(b > 0, errorMessage); + uint256 c = a / b; + // assert(a == b * c + a % b); // There is no case in which this doesn't hold + + return c; + } + + /** + * @dev Returns the remainder of dividing two unsigned integers. (unsigned integer modulo), + * Reverts when dividing by zero. + * + * Counterpart to Solidity's `%` operator. This function uses a `revert` + * opcode (which leaves remaining gas untouched) while Solidity uses an + * invalid opcode to revert (consuming all remaining gas). + * + * Requirements: + * - The divisor cannot be zero. + */ + function mod(uint256 a, uint256 b) internal pure returns (uint256) { + return mod(a, b, "SafeMath: modulo by zero"); + } + + /** + * @dev Returns the remainder of dividing two unsigned integers. (unsigned integer modulo), + * Reverts with custom message when dividing by zero. + * + * Counterpart to Solidity's `%` operator. This function uses a `revert` + * opcode (which leaves remaining gas untouched) while Solidity uses an + * invalid opcode to revert (consuming all remaining gas). + * + * Requirements: + * - The divisor cannot be zero. + * + * _Available since v2.4.0._ + */ + function mod( + uint256 a, + uint256 b, + string memory errorMessage + ) internal pure returns (uint256) { + require(b != 0, errorMessage); + return a % b; + } +} + +contract ERC20 is Context, IERC20 { + using SafeMath for uint256; + + mapping(address => uint256) private _balances; + + mapping(address => mapping(address => uint256)) private _allowances; + + uint256 private _totalSupply; + + /** + * @dev See {IERC20-totalSupply}. + */ + function totalSupply() public view returns (uint256) { + return _totalSupply; + } + + /** + * @dev See {IERC20-balanceOf}. + */ + function balanceOf(address account) public view returns (uint256) { + return _balances[account]; + } + + /** + * @dev See {IERC20-transfer}. + * + * Requirements: + * + * - `recipient` cannot be the zero address. + * - the caller must have a balance of at least `amount`. + */ + function transfer(address recipient, uint256 amount) public returns (bool) { + _transfer(_msgSender(), recipient, amount); + return true; + } + + /** + * @dev See {IERC20-allowance}. + */ + function allowance(address owner, address spender) public view returns (uint256) { + return _allowances[owner][spender]; + } + + /** + * @dev See {IERC20-approve}. + * + * Requirements: + * + * - `spender` cannot be the zero address. + */ + function approve(address spender, uint256 amount) public returns (bool) { + _approve(_msgSender(), spender, amount); + return true; + } + + /** + * @dev See {IERC20-transferFrom}. + * + * Emits an {Approval} event indicating the updated allowance. This is not + * required by the EIP. See the note at the beginning of {ERC20}; + * + * Requirements: + * - `sender` and `recipient` cannot be the zero address. + * - `sender` must have a balance of at least `amount`. + * - the caller must have allowance for `sender`'s tokens of at least + * `amount`. + */ + function transferFrom( + address sender, + address recipient, + uint256 amount + ) public returns (bool) { + _transfer(sender, recipient, amount); + _approve( + sender, + _msgSender(), + _allowances[sender][_msgSender()].sub( + amount, + "ERC20: transfer amount exceeds allowance" + ) + ); + return true; + } + + /** + * @dev Atomically increases the allowance granted to `spender` by the caller. + * + * This is an alternative to {approve} that can be used as a mitigation for + * problems described in {IERC20-approve}. + * + * Emits an {Approval} event indicating the updated allowance. + * + * Requirements: + * + * - `spender` cannot be the zero address. + */ + function increaseAllowance(address spender, uint256 addedValue) public returns (bool) { + _approve(_msgSender(), spender, _allowances[_msgSender()][spender].add(addedValue)); + return true; + } + + /** + * @dev Atomically decreases the allowance granted to `spender` by the caller. + * + * This is an alternative to {approve} that can be used as a mitigation for + * problems described in {IERC20-approve}. + * + * Emits an {Approval} event indicating the updated allowance. + * + * Requirements: + * + * - `spender` cannot be the zero address. + * - `spender` must have allowance for the caller of at least + * `subtractedValue`. + */ + function decreaseAllowance(address spender, uint256 subtractedValue) public returns (bool) { + _approve( + _msgSender(), + spender, + _allowances[_msgSender()][spender].sub( + subtractedValue, + "ERC20: decreased allowance below zero" + ) + ); + return true; + } + + /** + * @dev Moves tokens `amount` from `sender` to `recipient`. + * + * This is internal function is equivalent to {transfer}, and can be used to + * e.g. implement automatic token fees, slashing mechanisms, etc. + * + * Emits a {Transfer} event. + * + * Requirements: + * + * - `sender` cannot be the zero address. + * - `recipient` cannot be the zero address. + * - `sender` must have a balance of at least `amount`. + */ + function _transfer( + address sender, + address recipient, + uint256 amount + ) internal { + require(sender != address(0), "ERC20: transfer from the zero address"); + require(recipient != address(0), "ERC20: transfer to the zero address"); + + _balances[sender] = _balances[sender].sub(amount, "ERC20: transfer amount exceeds balance"); + _balances[recipient] = _balances[recipient].add(amount); + emit Transfer(sender, recipient, amount); + } + + /** @dev Creates `amount` tokens and assigns them to `account`, increasing + * the total supply. + * + * Emits a {Transfer} event with `from` set to the zero address. + * + * Requirements + * + * - `to` cannot be the zero address. + */ + function _mint(address account, uint256 amount) internal { + require(account != address(0), "ERC20: mint to the zero address"); + + _totalSupply = _totalSupply.add(amount); + _balances[account] = _balances[account].add(amount); + emit Transfer(address(0), account, amount); + } + + /** + * @dev Destroys `amount` tokens from `account`, reducing the + * total supply. + * + * Emits a {Transfer} event with `to` set to the zero address. + * + * Requirements + * + * - `account` cannot be the zero address. + * - `account` must have at least `amount` tokens. + */ + function _burn(address account, uint256 amount) internal { + require(account != address(0), "ERC20: burn from the zero address"); + + _balances[account] = _balances[account].sub(amount, "ERC20: burn amount exceeds balance"); + _totalSupply = _totalSupply.sub(amount); + emit Transfer(account, address(0), amount); + } + + /** + * @dev Sets `amount` as the allowance of `spender` over the `owner`s tokens. + * + * This is internal function is equivalent to `approve`, and can be used to + * e.g. set automatic allowances for certain subsystems, etc. + * + * Emits an {Approval} event. + * + * Requirements: + * + * - `owner` cannot be the zero address. + * - `spender` cannot be the zero address. + */ + function _approve( + address owner, + address spender, + uint256 amount + ) internal { + require(owner != address(0), "ERC20: approve from the zero address"); + require(spender != address(0), "ERC20: approve to the zero address"); + + _allowances[owner][spender] = amount; + emit Approval(owner, spender, amount); + } + + /** + * @dev Destroys `amount` tokens from `account`.`amount` is then deducted + * from the caller's allowance. + * + * See {_burn} and {_approve}. + */ + function _burnFrom(address account, uint256 amount) internal { + _burn(account, amount); + _approve( + account, + _msgSender(), + _allowances[account][_msgSender()].sub(amount, "ERC20: burn amount exceeds allowance") + ); + } +} + +contract InitializableERC20Detailed is IERC20 { + string private _name; + string private _symbol; + uint8 private _decimals; + + /** + * @dev Sets the values for `name`, `symbol`, and `decimals`. All three of + * these values are immutable: they can only be set once during + * construction. + * @notice To avoid variable shadowing appended `Arg` after arguments name. + */ + function _initialize( + string memory nameArg, + string memory symbolArg, + uint8 decimalsArg + ) internal { + _name = nameArg; + _symbol = symbolArg; + _decimals = decimalsArg; + } + + /** + * @dev Returns the name of the token. + */ + function name() public view returns (string memory) { + return _name; + } + + /** + * @dev Returns the symbol of the token, usually a shorter version of the + * name. + */ + function symbol() public view returns (string memory) { + return _symbol; + } + + /** + * @dev Returns the number of decimals used to get its user representation. + * For example, if `decimals` equals `2`, a balance of `505` tokens should + * be displayed to a user as `5,05` (`505 / 10 ** 2`). + * + * Tokens usually opt for a value of 18, imitating the relationship between + * Ether and Wei. + * + * NOTE: This information is only used for _display_ purposes: it in + * no way affects any of the arithmetic of the contract, including + * {IERC20-balanceOf} and {IERC20-transfer}. + */ + function decimals() public view returns (uint8) { + return _decimals; + } +} + +contract InitializableToken is ERC20, InitializableERC20Detailed { + /** + * @dev Initialization function for implementing contract + * @notice To avoid variable shadowing appended `Arg` after arguments name. + */ + function _initialize(string memory _nameArg, string memory _symbolArg) internal { + InitializableERC20Detailed._initialize(_nameArg, _symbolArg, 18); + } +} + +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; +} + +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; +} + +contract InitializableModule2 is ModuleKeys { + INexus public constant nexus = INexus(0xAFcE80b19A8cE13DEc0739a1aaB7A028d6845Eb3); + + /** + * @dev Modifier to allow function calls only from the Governor. + */ + modifier onlyGovernor() { + 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 Modifier to allow function calls only from the ProxyAdmin. + */ + modifier onlyProxyAdmin() { + require(msg.sender == _proxyAdmin(), "Only ProxyAdmin can execute"); + _; + } + + /** + * @dev Modifier to allow function calls only from the Manager. + */ + modifier onlyManager() { + require(msg.sender == _manager(), "Only manager 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 Staking Module address from the Nexus + * @return Address of the Staking Module contract + */ + function _staking() internal view returns (address) { + return nexus.getModule(KEY_STAKING); + } + + /** + * @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); + } + + /** + * @dev Return MetaToken Module address from the Nexus + * @return Address of the MetaToken Module contract + */ + function _metaToken() internal view returns (address) { + return nexus.getModule(KEY_META_TOKEN); + } + + /** + * @dev Return OracleHub Module address from the Nexus + * @return Address of the OracleHub Module contract + */ + function _oracleHub() internal view returns (address) { + return nexus.getModule(KEY_ORACLE_HUB); + } + + /** + * @dev Return Manager Module address from the Nexus + * @return Address of the Manager Module contract + */ + function _manager() internal view returns (address) { + return nexus.getModule(KEY_MANAGER); + } + + /** + * @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); + } +} + +interface IConnector { + /** + * @notice Deposits the mAsset into the connector + * @param _amount Units of mAsset to receive and deposit + */ + function deposit(uint256 _amount) external; + + /** + * @notice Withdraws a specific amount of mAsset from the connector + * @param _amount Units of mAsset to withdraw + */ + function withdraw(uint256 _amount) external; + + /** + * @notice Withdraws all mAsset from the connector + */ + function withdrawAll() external; + + /** + * @notice Returns the available balance in the connector. In connections + * where there is likely to be an initial dip in value due to conservative + * exchange rates (e.g. with Curves `get_virtual_price`), it should return + * max(deposited, balance) to avoid temporary negative yield. Any negative yield + * should be corrected during a withdrawal or over time. + * @return Balance of mAsset in the connector + */ + function checkBalance() external view returns (uint256); +} + +contract Initializable { + /** + * @dev Indicates that the contract has been initialized. + */ + bool private initialized; + + /** + * @dev Indicates that the contract is in the process of being initialized. + */ + bool private initializing; + + /** + * @dev Modifier to use in the initializer function of a contract. + */ + modifier initializer() { + require( + initializing || isConstructor() || !initialized, + "Contract instance has already been initialized" + ); + + bool isTopLevelCall = !initializing; + if (isTopLevelCall) { + initializing = true; + initialized = true; + } + + _; + + if (isTopLevelCall) { + initializing = false; + } + } + + /// @dev Returns true if and only if the function is running in the constructor + function isConstructor() private view returns (bool) { + // extcodesize checks the size of the code stored in an address, and + // address returns the current address. Since the code is still not + // deployed when running a constructor, any checks on its code size will + // yield zero, making it an effective way to detect if a contract is + // under construction or not. + address self = address(this); + uint256 cs; + assembly { + cs := extcodesize(self) + } + return cs == 0; + } + + // Reserved storage space to allow for layout changes in the future. + uint256[50] private ______gap; +} + +library StableMath { + using SafeMath for uint256; + + /** + * @dev Scaling unit for use in specific calculations, + * where 1 * 10**18, or 1e18 represents a unit '1' + */ + uint256 private constant FULL_SCALE = 1e18; + + /** + * @notice 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) + * @dev 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.mul(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 + uint256 z = x.mul(y); + // return 9e38 / 1e18 = 9e18 + return z.div(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.mul(y); + // e.g. 138145381104e17 + 9.99...e17 = 138145381113.99...e17 + uint256 ceil = scaled.add(FULL_SCALE.sub(1)); + // e.g. 13814538111.399...e18 / 1e18 = 13814538111 + return ceil.div(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 + uint256 z = x.mul(FULL_SCALE); + // e.g. 8e36 / 10e18 = 8e17 + return z.div(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 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.mul(ratio); + // 1e26 + 9.99e7 = 100..00.999e8 + uint256 ceil = scaled.add(RATIO_SCALE.sub(1)); + // return 100..00.999e8 / 1e8 = 1e18 + return ceil.div(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 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 + uint256 y = x.mul(RATIO_SCALE); + // return 1e22 / 1e12 = 1e10 + return y.div(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; + } +} + +/** + * @title SavingsContract + * @author Stability Labs Pty. Ltd. + * @notice Savings contract uses the ever increasing "exchangeRate" to increase + * the value of the Savers "credits" (ERC20) relative to the amount of additional + * underlying collateral that has been deposited into this contract ("interest") + * @dev VERSION: 2.1 + * DATE: 2021-11-25 + */ +contract SavingsContract_imusd_mainnet_21 is + ISavingsContractV1, + ISavingsContractV3, + Initializable, + InitializableToken, + InitializableModule2 +{ + using SafeMath for uint256; + using StableMath for uint256; + + // Core events for depositing and withdrawing + event ExchangeRateUpdated(uint256 newExchangeRate, uint256 interestCollected); + event SavingsDeposited(address indexed saver, uint256 savingsDeposited, uint256 creditsIssued); + event CreditsRedeemed( + address indexed redeemer, + uint256 creditsRedeemed, + uint256 savingsCredited + ); + + event AutomaticInterestCollectionSwitched(bool automationEnabled); + + // Connector poking + event PokerUpdated(address poker); + + event FractionUpdated(uint256 fraction); + event ConnectorUpdated(address connector); + event EmergencyUpdate(); + + event Poked(uint256 oldBalance, uint256 newBalance, uint256 interestDetected); + event PokedRaw(); + + // Tracking events + event Referral(address indexed referrer, address beneficiary, uint256 amount); + + // Rate between 'savings credits' and underlying + // e.g. 1 credit (1e17) mulTruncate(exchangeRate) = underlying, starts at 10:1 + // exchangeRate increases over time + uint256 private constant startingRate = 1e17; + uint256 public exchangeRate; + + // Underlying asset is underlying + IERC20 public constant underlying = IERC20(0xe2f2a5C287993345a840Db3B0845fbC70f5935a5); + bool private automateInterestCollection; + + // Yield + // Poker is responsible for depositing/withdrawing from connector + address public poker; + // Last time a poke was made + uint256 public lastPoke; + // Last known balance of the connector + uint256 public lastBalance; + // Fraction of capital assigned to the connector (100% = 1e18) + uint256 public fraction; + // Address of the current connector (all IConnectors are mStable validated) + IConnector public connector; + // How often do we allow pokes + uint256 private constant POKE_CADENCE = 4 hours; + // Max APY generated on the capital in the connector + uint256 private constant MAX_APY = 4e18; + uint256 private constant SECONDS_IN_YEAR = 365 days; + // Proxy contract for easy redemption + address public constant unwrapper = 0xc1443Cb9ce81915fB914C270d74B0D57D1c87be0; + + // Add these constants to bytecode at deploytime + function initialize( + address _poker, + string calldata _nameArg, + string calldata _symbolArg + ) external initializer { + InitializableToken._initialize(_nameArg, _symbolArg); + + require(_poker != address(0), "Invalid poker address"); + poker = _poker; + + fraction = 2e17; + automateInterestCollection = true; + exchangeRate = startingRate; + } + + /** @dev Only the savings managaer (pulled from Nexus) can execute this */ + modifier onlySavingsManager() { + require(msg.sender == _savingsManager(), "Only savings manager can execute"); + _; + } + + /*************************************** + VIEW - E + ****************************************/ + + /** + * @dev Returns the underlying balance of a given user + * @param _user Address of the user to check + * @return balance Units of underlying owned by the user + */ + function balanceOfUnderlying(address _user) external view returns (uint256 balance) { + (balance, ) = _creditsToUnderlying(balanceOf(_user)); + } + + /** + * @dev Converts a given underlying amount into credits + * @param _underlying Units of underlying + * @return credits Credit units (a.k.a imUSD) + */ + function underlyingToCredits(uint256 _underlying) external view returns (uint256 credits) { + (credits, ) = _underlyingToCredits(_underlying); + } + + /** + * @dev Converts a given credit amount into underlying + * @param _credits Units of credits + * @return amount Corresponding underlying amount + */ + function creditsToUnderlying(uint256 _credits) external view returns (uint256 amount) { + (amount, ) = _creditsToUnderlying(_credits); + } + + // Deprecated in favour of `balanceOf(address)` + // Maintained for backwards compatibility + // Returns the credit balance of a given user + function creditBalances(address _user) external view returns (uint256) { + return balanceOf(_user); + } + + /*************************************** + INTEREST + ****************************************/ + + /** + * @dev Deposit interest (add to savings) and update exchange rate of contract. + * Exchange rate is calculated as the ratio between new savings q and credits: + * exchange rate = savings / credits + * + * @param _amount Units of underlying to add to the savings vault + */ + function depositInterest(uint256 _amount) external onlySavingsManager { + require(_amount > 0, "Must deposit something"); + + // Transfer the interest from sender to here + require(underlying.transferFrom(msg.sender, address(this), _amount), "Must receive tokens"); + + // Calc new exchange rate, protect against initialisation case + uint256 totalCredits = totalSupply(); + if (totalCredits > 0) { + // new exchange rate is relationship between _totalCredits & totalSavings + // _totalCredits * exchangeRate = totalSavings + // exchangeRate = totalSavings/_totalCredits + (uint256 totalCollat, ) = _creditsToUnderlying(totalCredits); + uint256 newExchangeRate = _calcExchangeRate(totalCollat.add(_amount), totalCredits); + exchangeRate = newExchangeRate; + + emit ExchangeRateUpdated(newExchangeRate, _amount); + } + } + + /** @dev Enable or disable the automation of fee collection during deposit process */ + function automateInterestCollectionFlag(bool _enabled) external onlyGovernor { + automateInterestCollection = _enabled; + emit AutomaticInterestCollectionSwitched(_enabled); + } + + /*************************************** + DEPOSIT + ****************************************/ + + /** + * @dev During a migration period, allow savers to deposit underlying here before the interest has been redirected + * @param _underlying Units of underlying to deposit into savings vault + * @param _beneficiary Immediately transfer the imUSD token to this beneficiary address + * @return creditsIssued Units of credits (imUSD) issued + */ + function preDeposit(uint256 _underlying, address _beneficiary) + external + returns (uint256 creditsIssued) + { + require(exchangeRate == startingRate, "Can only use this method before streaming begins"); + return _deposit(_underlying, _beneficiary, false); + } + + /** + * @dev Deposit the senders savings to the vault, and credit them internally with "credits". + * Credit amount is calculated as a ratio of deposit amount and exchange rate: + * credits = underlying / exchangeRate + * We will first update the internal exchange rate by collecting any interest generated on the underlying. + * @param _underlying Units of underlying to deposit into savings vault + * @return creditsIssued Units of credits (imUSD) issued + */ + function depositSavings(uint256 _underlying) external returns (uint256 creditsIssued) { + return _deposit(_underlying, msg.sender, true); + } + + /** + * @dev Deposit the senders savings to the vault, and credit them internally with "credits". + * Credit amount is calculated as a ratio of deposit amount and exchange rate: + * credits = underlying / exchangeRate + * We will first update the internal exchange rate by collecting any interest generated on the underlying. + * @param _underlying Units of underlying to deposit into savings vault + * @param _beneficiary Immediately transfer the imUSD token to this beneficiary address + * @return creditsIssued Units of credits (imUSD) issued + */ + function depositSavings(uint256 _underlying, address _beneficiary) + external + returns (uint256 creditsIssued) + { + return _deposit(_underlying, _beneficiary, true); + } + + /** + * @dev Overloaded `depositSavings` method with an optional referrer address. + * @param _underlying Units of underlying to deposit into savings vault + * @param _beneficiary Immediately transfer the imUSD token to this beneficiary address + * @param _referrer Referrer address for this deposit + * @return creditsIssued Units of credits (imUSD) issued + */ + function depositSavings( + uint256 _underlying, + address _beneficiary, + address _referrer + ) external returns (uint256 creditsIssued) { + emit Referral(_referrer, _beneficiary, _underlying); + return _deposit(_underlying, _beneficiary, true); + } + + /** + * @dev Internally deposit the _underlying from the sender and credit the beneficiary with new imUSD + */ + function _deposit( + uint256 _underlying, + address _beneficiary, + bool _collectInterest + ) internal returns (uint256 creditsIssued) { + require(_underlying > 0, "Must deposit something"); + require(_beneficiary != address(0), "Invalid beneficiary address"); + + // Collect recent interest generated by basket and update exchange rate + IERC20 mAsset = underlying; + if (_collectInterest) { + ISavingsManager(_savingsManager()).collectAndDistributeInterest(address(mAsset)); + } + + // Transfer tokens from sender to here + require(mAsset.transferFrom(msg.sender, address(this), _underlying), "Must receive tokens"); + + // Calc how many credits they receive based on currentRatio + (creditsIssued, ) = _underlyingToCredits(_underlying); + + // add credits to ERC20 balances + _mint(_beneficiary, creditsIssued); + + emit SavingsDeposited(_beneficiary, _underlying, creditsIssued); + } + + /*************************************** + REDEEM + ****************************************/ + + // Deprecated in favour of redeemCredits + // Maintaining backwards compatibility, this fn minimics the old redeem fn, in which + // credits are redeemed but the interest from the underlying is not collected. + function redeem(uint256 _credits) external returns (uint256 massetReturned) { + require(_credits > 0, "Must withdraw something"); + + (, uint256 payout) = _redeem(_credits, true, true); + + // Collect recent interest generated by basket and update exchange rate + if (automateInterestCollection) { + ISavingsManager(_savingsManager()).collectAndDistributeInterest(address(underlying)); + } + + return payout; + } + + /** + * @dev Redeem specific number of the senders "credits" in exchange for underlying. + * Payout amount is calculated as a ratio of credits and exchange rate: + * payout = credits * exchangeRate + * @param _credits Amount of credits to redeem + * @return massetReturned Units of underlying mAsset paid out + */ + function redeemCredits(uint256 _credits) external returns (uint256 massetReturned) { + require(_credits > 0, "Must withdraw something"); + + // Collect recent interest generated by basket and update exchange rate + if (automateInterestCollection) { + ISavingsManager(_savingsManager()).collectAndDistributeInterest(address(underlying)); + } + + (, uint256 payout) = _redeem(_credits, true, true); + + return payout; + } + + /** + * @dev Redeem credits into a specific amount of underlying. + * Credits needed to burn is calculated using: + * credits = underlying / exchangeRate + * @param _underlying Amount of underlying to redeem + * @return creditsBurned Units of credits burned from sender + */ + function redeemUnderlying(uint256 _underlying) external returns (uint256 creditsBurned) { + require(_underlying > 0, "Must withdraw something"); + + // Collect recent interest generated by basket and update exchange rate + if (automateInterestCollection) { + ISavingsManager(_savingsManager()).collectAndDistributeInterest(address(underlying)); + } + + // Ensure that the payout was sufficient + (uint256 credits, uint256 massetReturned) = _redeem(_underlying, false, true); + require(massetReturned == _underlying, "Invalid output"); + + return credits; + } + + /** + * @notice Redeem credits into a specific amount of underlying, unwrap + * into a selected output asset, and send to a beneficiary + * Credits needed to burn is calculated using: + * credits = underlying / exchangeRate + * @param _amount Units to redeem (either underlying or credit amount). + * @param _isCreditAmt `true` if `amount` is in credits. eg imUSD. `false` if `amount` is in underlying. eg mUSD. + * @param _minAmountOut Minimum amount of `output` tokens to unwrap for. This is to the same decimal places as the `output` token. + * @param _output Asset to receive in exchange for the redeemed mAssets. This can be a bAsset or a fAsset. For example: + - bAssets (USDC, DAI, sUSD or USDT) or fAssets (GUSD, BUSD, alUSD, FEI or RAI) for mainnet imUSD Vault. + - bAssets (USDC, DAI or USDT) or fAsset FRAX for Polygon imUSD Vault. + - bAssets (WBTC, sBTC or renBTC) or fAssets (HBTC or TBTCV2) for mainnet imBTC Vault. + * @param _beneficiary Address to send `output` tokens to. + * @param _router mAsset address if the output is a bAsset. Feeder Pool address if the output is a fAsset. + * @param _isBassetOut `true` if `output` is a bAsset. `false` if `output` is a fAsset. + * @return creditsBurned Units of credits burned from sender. eg imUSD or imBTC. + * @return massetReturned Units of the underlying mAssets that were redeemed or swapped for the output tokens. eg mUSD or mBTC. + * @return outputQuantity Units of `output` tokens sent to the beneficiary. + */ + function redeemAndUnwrap( + uint256 _amount, + bool _isCreditAmt, + uint256 _minAmountOut, + address _output, + address _beneficiary, + address _router, + bool _isBassetOut + ) + external + returns ( + uint256 creditsBurned, + uint256 massetReturned, + uint256 outputQuantity + ) + { + require(_amount > 0, "Must withdraw something"); + require(_output != address(0), "Output address is zero"); + require(_beneficiary != address(0), "Beneficiary address is zero"); + require(_router != address(0), "Router address is zero"); + + // Collect recent interest generated by basket and update exchange rate + if (automateInterestCollection) { + ISavingsManager(_savingsManager()).collectAndDistributeInterest(address(underlying)); + } + + // Ensure that the payout was sufficient + (creditsBurned, massetReturned) = _redeem(_amount, _isCreditAmt, false); + require( + _isCreditAmt ? creditsBurned == _amount : massetReturned == _amount, + "Invalid output" + ); + + // Approve wrapper to spend contract's underlying; just for this tx + underlying.approve(unwrapper, massetReturned); + + // Unwrap the underlying into `output` and transfer to `beneficiary` + outputQuantity = IUnwrapper(unwrapper).unwrapAndSend( + _isBassetOut, + _router, + address(underlying), + _output, + massetReturned, + _minAmountOut, + _beneficiary + ); + } + + /** + * @dev Internally burn the credits and send the underlying to msg.sender + */ + function _redeem( + uint256 _amt, + bool _isCreditAmt, + bool _transferUnderlying + ) internal returns (uint256 creditsBurned, uint256 massetReturned) { + // Centralise credit <> underlying calcs and minimise SLOAD count + uint256 credits_; + uint256 underlying_; + uint256 exchangeRate_; + // If the input is a credit amt, then calculate underlying payout and cache the exchangeRate + if (_isCreditAmt) { + credits_ = _amt; + (underlying_, exchangeRate_) = _creditsToUnderlying(_amt); + } + // If the input is in underlying, then calculate credits needed to burn + else { + underlying_ = _amt; + (credits_, exchangeRate_) = _underlyingToCredits(_amt); + } + + // Burn required credits from the sender FIRST + _burn(msg.sender, credits_); + // Optionally, transfer tokens from here to sender + if (_transferUnderlying) { + require(underlying.transfer(msg.sender, underlying_), "Must send tokens"); + } + + // If this withdrawal pushes the portion of stored collateral in the `connector` over a certain + // threshold (fraction + 20%), then this should trigger a _poke on the connector. This is to avoid + // a situation in which there is a rush on withdrawals for some reason, causing the connector + // balance to go up and thus having too large an exposure. + CachedData memory cachedData = _cacheData(); + ConnectorStatus memory status = _getConnectorStatus(cachedData, exchangeRate_); + if (status.inConnector > status.limit) { + _poke(cachedData, false); + } + + emit CreditsRedeemed(msg.sender, credits_, underlying_); + + return (credits_, underlying_); + } + + struct ConnectorStatus { + // Limit is the max amount of units allowed in the connector + uint256 limit; + // Derived balance of the connector + uint256 inConnector; + } + + /** + * @dev Derives the units of collateral held in the connector + * @param _data Struct containing data on balances + * @param _exchangeRate Current system exchange rate + * @return status Contains max amount of assets allowed in connector + */ + function _getConnectorStatus(CachedData memory _data, uint256 _exchangeRate) + internal + pure + returns (ConnectorStatus memory) + { + // Total units of underlying collateralised + uint256 totalCollat = _data.totalCredits.mulTruncate(_exchangeRate); + // Max amount of underlying that can be held in the connector + uint256 limit = totalCollat.mulTruncate(_data.fraction.add(2e17)); + // Derives amount of underlying present in the connector + uint256 inConnector = _data.rawBalance >= totalCollat + ? 0 + : totalCollat.sub(_data.rawBalance); + + return ConnectorStatus(limit, inConnector); + } + + /*************************************** + YIELD - E + ****************************************/ + + /** @dev Modifier allowing only the designated poker to execute the fn */ + modifier onlyPoker() { + require(msg.sender == poker, "Only poker can execute"); + _; + } + + /** + * @dev External poke function allows for the redistribution of collateral between here and the + * current connector, setting the ratio back to the defined optimal. + */ + function poke() external onlyPoker { + CachedData memory cachedData = _cacheData(); + _poke(cachedData, false); + } + + /** + * @dev Governance action to set the address of a new poker + * @param _newPoker Address of the new poker + */ + function setPoker(address _newPoker) external onlyGovernor { + require(_newPoker != address(0) && _newPoker != poker, "Invalid poker"); + + poker = _newPoker; + + emit PokerUpdated(_newPoker); + } + + /** + * @dev Governance action to set the percentage of assets that should be held + * in the connector. + * @param _fraction Percentage of assets that should be held there (where 20% == 2e17) + */ + function setFraction(uint256 _fraction) external onlyGovernor { + require(_fraction <= 5e17, "Fraction must be <= 50%"); + + fraction = _fraction; + + CachedData memory cachedData = _cacheData(); + _poke(cachedData, true); + + emit FractionUpdated(_fraction); + } + + /** + * @dev Governance action to set the address of a new connector, and move funds (if any) across. + * @param _newConnector Address of the new connector + */ + function setConnector(address _newConnector) external onlyGovernor { + // Withdraw all from previous by setting target = 0 + CachedData memory cachedData = _cacheData(); + cachedData.fraction = 0; + _poke(cachedData, true); + + // Set new connector + CachedData memory cachedDataNew = _cacheData(); + connector = IConnector(_newConnector); + _poke(cachedDataNew, true); + + emit ConnectorUpdated(_newConnector); + } + + /** + * @dev Governance action to perform an emergency withdraw of the assets in the connector, + * should it be the case that some or all of the liquidity is trapped in. This causes the total + * collateral in the system to go down, causing a hard refresh. + */ + function emergencyWithdraw(uint256 _withdrawAmount) external onlyGovernor { + // withdraw _withdrawAmount from connection + connector.withdraw(_withdrawAmount); + + // reset the connector + connector = IConnector(address(0)); + emit ConnectorUpdated(address(0)); + + // set fraction to 0 + fraction = 0; + emit FractionUpdated(0); + + // check total collateralisation of credits + CachedData memory data = _cacheData(); + // use rawBalance as the remaining liquidity in the connector is now written off + _refreshExchangeRate(data.rawBalance, data.totalCredits, true); + + emit EmergencyUpdate(); + } + + /*************************************** + YIELD - I + ****************************************/ + + /** @dev Internal poke function to keep the balance between connector and raw balance healthy */ + function _poke(CachedData memory _data, bool _ignoreCadence) internal { + require(_data.totalCredits > 0, "Must have something to poke"); + + // 1. Verify that poke cadence is valid, unless this is a manual action by governance + uint256 currentTime = uint256(now); + uint256 timeSinceLastPoke = currentTime.sub(lastPoke); + require(_ignoreCadence || timeSinceLastPoke > POKE_CADENCE, "Not enough time elapsed"); + lastPoke = currentTime; + + // If there is a connector, check the balance and settle to the specified fraction % + IConnector connector_ = connector; + if (address(connector_) != address(0)) { + // 2. Check and verify new connector balance + uint256 lastBalance_ = lastBalance; + uint256 connectorBalance = connector_.checkBalance(); + // Always expect the collateral in the connector to increase in value + require(connectorBalance >= lastBalance_, "Invalid yield"); + if (connectorBalance > 0) { + // Validate the collection by ensuring that the APY is not ridiculous + _validateCollection( + connectorBalance, + connectorBalance.sub(lastBalance_), + timeSinceLastPoke + ); + } + + // 3. Level the assets to Fraction (connector) & 100-fraction (raw) + uint256 sum = _data.rawBalance.add(connectorBalance); + uint256 ideal = sum.mulTruncate(_data.fraction); + // If there is not enough mAsset in the connector, then deposit + if (ideal > connectorBalance) { + uint256 deposit = ideal.sub(connectorBalance); + underlying.approve(address(connector_), deposit); + connector_.deposit(deposit); + } + // Else withdraw, if there is too much mAsset in the connector + else if (connectorBalance > ideal) { + // If fraction == 0, then withdraw everything + if (ideal == 0) { + connector_.withdrawAll(); + sum = IERC20(underlying).balanceOf(address(this)); + } else { + connector_.withdraw(connectorBalance.sub(ideal)); + } + } + // Else ideal == connectorBalance (e.g. 0), do nothing + require(connector_.checkBalance() >= ideal, "Enforce system invariant"); + + // 4i. Refresh exchange rate and emit event + lastBalance = ideal; + _refreshExchangeRate(sum, _data.totalCredits, false); + emit Poked(lastBalance_, ideal, connectorBalance.sub(lastBalance_)); + } else { + // 4ii. Refresh exchange rate and emit event + lastBalance = 0; + _refreshExchangeRate(_data.rawBalance, _data.totalCredits, false); + emit PokedRaw(); + } + } + + /** + * @dev Internal fn to refresh the exchange rate, based on the sum of collateral and the number of credits + * @param _realSum Sum of collateral held by the contract + * @param _totalCredits Total number of credits in the system + * @param _ignoreValidation This is for use in the emergency situation, and ignores a decreasing exchangeRate + */ + function _refreshExchangeRate( + uint256 _realSum, + uint256 _totalCredits, + bool _ignoreValidation + ) internal { + // Based on the current exchange rate, how much underlying is collateralised? + (uint256 totalCredited, ) = _creditsToUnderlying(_totalCredits); + + // Require the amount of capital held to be greater than the previously credited units + require(_ignoreValidation || _realSum >= totalCredited, "ExchangeRate must increase"); + // Work out the new exchange rate based on the current capital + uint256 newExchangeRate = _calcExchangeRate(_realSum, _totalCredits); + exchangeRate = newExchangeRate; + + emit ExchangeRateUpdated( + newExchangeRate, + _realSum > totalCredited ? _realSum.sub(totalCredited) : 0 + ); + } + + /** + * FORKED DIRECTLY FROM SAVINGSMANAGER.sol + * --------------------------------------- + * @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 _newBalance New balance of the underlying + * @param _interest Increase in total supply since last collection + * @param _timeSinceLastCollection Seconds since last collection + */ + function _validateCollection( + uint256 _newBalance, + uint256 _interest, + uint256 _timeSinceLastCollection + ) internal pure returns (uint256 extrapolatedAPY) { + // Protect against division by 0 + uint256 protectedTime = StableMath.max(1, _timeSinceLastCollection); + + uint256 oldSupply = _newBalance.sub(_interest); + uint256 percentageIncrease = _interest.divPrecisely(oldSupply); + + uint256 yearsSinceLastCollection = protectedTime.divPrecisely(SECONDS_IN_YEAR); + + extrapolatedAPY = percentageIncrease.divPrecisely(yearsSinceLastCollection); + + if (protectedTime > 30 minutes) { + require(extrapolatedAPY < MAX_APY, "Interest protected from inflating past maxAPY"); + } else { + require(percentageIncrease < 1e15, "Interest protected from inflating past 10 Bps"); + } + } + + /*************************************** + VIEW - I + ****************************************/ + + struct CachedData { + // SLOAD from 'fraction' + uint256 fraction; + // ERC20 balance of underlying, held by this contract + // underlying.balanceOf(address(this)) + uint256 rawBalance; + // totalSupply() + uint256 totalCredits; + } + + /** + * @dev Retrieves generic data to avoid duplicate SLOADs + */ + function _cacheData() internal view returns (CachedData memory) { + uint256 balance = underlying.balanceOf(address(this)); + return CachedData(fraction, balance, totalSupply()); + } + + /** + * @dev Converts masset amount into credits based on exchange rate + * c = (masset / exchangeRate) + 1 + */ + function _underlyingToCredits(uint256 _underlying) + internal + view + returns (uint256 credits, uint256 exchangeRate_) + { + // e.g. (1e20 * 1e18) / 1e18 = 1e20 + // e.g. (1e20 * 1e18) / 14e17 = 7.1429e19 + // e.g. 1 * 1e18 / 1e17 + 1 = 11 => 11 * 1e17 / 1e18 = 1.1e18 / 1e18 = 1 + exchangeRate_ = exchangeRate; + credits = _underlying.divPrecisely(exchangeRate_).add(1); + } + + /** + * @dev Works out a new exchange rate, given an amount of collateral and total credits + * e = underlying / (credits-1) + */ + function _calcExchangeRate(uint256 _totalCollateral, uint256 _totalCredits) + internal + pure + returns (uint256 _exchangeRate) + { + _exchangeRate = _totalCollateral.divPrecisely(_totalCredits.sub(1)); + } + + /** + * @dev Converts credit amount into masset based on exchange rate + * m = credits * exchangeRate + */ + function _creditsToUnderlying(uint256 _credits) + internal + view + returns (uint256 underlyingAmount, uint256 exchangeRate_) + { + // e.g. (1e20 * 1e18) / 1e18 = 1e20 + // e.g. (1e20 * 14e17) / 1e18 = 1.4e20 + exchangeRate_ = exchangeRate; + underlyingAmount = _credits.mulTruncate(exchangeRate_); + } +} diff --git a/contracts/legacy-upgraded/imusd-mainnet-22.sol b/contracts/legacy-upgraded/imusd-mainnet-22.sol index 4434bde1..5e272e98 100644 --- a/contracts/legacy-upgraded/imusd-mainnet-22.sol +++ b/contracts/legacy-upgraded/imusd-mainnet-22.sol @@ -1333,7 +1333,8 @@ contract SavingsContract_imusd_mainnet_22 is uint256 private constant MAX_APY = 4e18; uint256 private constant SECONDS_IN_YEAR = 365 days; // Proxy contract for easy redemption - address public unwrapper; // TODO!! + address public constant unwrapper = 0xc1443Cb9ce81915fB914C270d74B0D57D1c87be0; + uint256 private constant MAX_INT256 = 2**256 - 1; // Add these constants to bytecode at deploytime function initialize( @@ -1503,7 +1504,6 @@ contract SavingsContract_imusd_mainnet_22 is bool _collectInterest ) internal returns (uint256 creditsIssued) { creditsIssued = _transferAndMint(_underlying, _beneficiary, _collectInterest); - emit SavingsDeposited(_beneficiary, _underlying, creditsIssued); } /*************************************** @@ -2026,7 +2026,7 @@ contract SavingsContract_imusd_mainnet_22 is function maxDeposit( address /** caller **/ ) external view returns (uint256 maxAssets) { - maxAssets = 2**256 - 1; + maxAssets = MAX_INT256; } /** @@ -2078,7 +2078,7 @@ contract SavingsContract_imusd_mainnet_22 is function maxMint( address /* caller */ ) external view returns (uint256 maxShares) { - maxShares = 2**256 - 1; + maxShares = MAX_INT256; } /** @@ -2103,6 +2103,24 @@ contract SavingsContract_imusd_mainnet_22 is _transferAndMint(assets, receiver, true); } + /** + * @notice Mint exact amount of vault shares to the receiver by transferring enough underlying asset tokens from the caller. + * @param shares The amount of vault shares to be minted. + * @param receiver The account the vault shares will be minted to. + * @param referrer Referrer address for this deposit. + * @return assets The amount of underlying assets that were transferred from the caller. + * Emits a {Deposit}, {Referral} events + */ + function mint( + uint256 shares, + address receiver, + address referrer + ) external returns (uint256 assets) { + (assets, ) = _creditsToUnderlying(shares); + _transferAndMint(assets, receiver, true); + emit Referral(referrer, receiver, assets); + } + /** * * Returns Total number of underlying assets that caller can withdraw. @@ -2206,6 +2224,7 @@ contract SavingsContract_imusd_mainnet_22 is // add credits to ERC20 balances _mint(receiver, shares); emit Deposit(msg.sender, receiver, assets, shares); + emit SavingsDeposited(receiver, assets, shares); } /*/////////////////////////////////////////////////////////////// @@ -2224,7 +2243,7 @@ contract SavingsContract_imusd_mainnet_22 is // If caller is not the owner of the shares uint256 allowed = allowance(owner, msg.sender); - if (msg.sender != owner && allowed != (2**256 - 1)) { + if (msg.sender != owner && allowed != MAX_INT256) { require(shares <= allowed, "Amount exceeds allowance"); _approve(owner, msg.sender, allowed - shares); } diff --git a/contracts/legacy-upgraded/imusd-polygon-22.sol b/contracts/legacy-upgraded/imusd-polygon-22.sol index 5372c6be..96d252d0 100644 --- a/contracts/legacy-upgraded/imusd-polygon-22.sol +++ b/contracts/legacy-upgraded/imusd-polygon-22.sol @@ -1368,7 +1368,6 @@ contract SavingsContract_imusd_polygon_22 is bool _collectInterest ) internal returns (uint256 creditsIssued) { creditsIssued = _transferAndMint(_underlying, _beneficiary, _collectInterest); - emit SavingsDeposited(_beneficiary, _underlying, creditsIssued); } /*************************************** @@ -1942,6 +1941,24 @@ contract SavingsContract_imusd_polygon_22 is _transferAndMint(assets, receiver, true); } + /** + * @notice Mint exact amount of vault shares to the receiver by transferring enough underlying asset tokens from the caller. + * @param shares The amount of vault shares to be minted. + * @param receiver The account the vault shares will be minted to. + * @param referrer Referrer address for this deposit. + * @return assets The amount of underlying assets that were transferred from the caller. + * Emits a {Deposit}, {Referral} events + */ + function mint( + uint256 shares, + address receiver, + address referrer + ) external returns (uint256 assets) { + (assets, ) = _creditsToUnderlying(shares); + _transferAndMint(assets, receiver, true); + emit Referral(referrer, receiver, assets); + } + /** * * Returns Total number of underlying assets that caller can withdraw. @@ -2045,6 +2062,7 @@ contract SavingsContract_imusd_polygon_22 is // add credits to ERC20 balances _mint(receiver, shares); emit Deposit(msg.sender, receiver, assets, shares); + emit SavingsDeposited(receiver, assets, shares); } /*/////////////////////////////////////////////////////////////// diff --git a/contracts/savings/SavingsContract.sol b/contracts/savings/SavingsContract.sol index 25adf15d..dadd431e 100644 --- a/contracts/savings/SavingsContract.sol +++ b/contracts/savings/SavingsContract.sol @@ -281,7 +281,6 @@ contract SavingsContract is bool _collectInterest ) internal returns (uint256 creditsIssued) { creditsIssued = _transferAndMint(_underlying, _beneficiary, _collectInterest); - emit SavingsDeposited(_beneficiary, _underlying, creditsIssued); } /*************************************** @@ -858,6 +857,24 @@ contract SavingsContract is _transferAndMint(assets, receiver, true); } + /** + * @notice Mint exact amount of vault shares to the receiver by transferring enough underlying asset tokens from the caller. + * @param shares The amount of vault shares to be minted. + * @param receiver The account the vault shares will be minted to. + * @param referrer Referrer address for this deposit. + * @return assets The amount of underlying assets that were transferred from the caller. + * Emits a {Deposit}, {Referral} events + */ + function mint( + uint256 shares, + address receiver, + address referrer + ) external returns (uint256 assets) { + (assets, ) = _creditsToUnderlying(shares); + _transferAndMint(assets, receiver, true); + emit Referral(referrer, receiver, assets); + } + /** * * Returns Total number of underlying assets that caller can withdraw. @@ -961,6 +978,7 @@ contract SavingsContract is // add credits to ERC20 balances _mint(receiver, shares); emit Deposit(msg.sender, receiver, assets, shares); + emit SavingsDeposited(receiver, assets, shares); } /*/////////////////////////////////////////////////////////////// diff --git a/tasks/deploySavingsContract4626.ts b/tasks/deploySavingsContract4626.ts new file mode 100644 index 00000000..63ee9d5c --- /dev/null +++ b/tasks/deploySavingsContract4626.ts @@ -0,0 +1,146 @@ +import "ts-node/register" +import "tsconfig-paths/register" +import { DEAD_ADDRESS } from "@utils/constants" +import { task, types } from "hardhat/config" +import { DelayedProxyAdmin__factory } from "types/generated" +// Polygon imUSD Contract +import { SavingsContractImusdPolygon22 } from "types/generated/SavingsContractImusdPolygon22" +import { SavingsContractImusdPolygon22__factory } from "types/generated/factories/SavingsContractImusdPolygon22__factory" +// Mainnet imBTC Contract +import { SavingsContractImbtcMainnet22__factory } from "types/generated/factories/SavingsContractImbtcMainnet22__factory" +import { SavingsContractImbtcMainnet22 } from "types/generated/SavingsContractImbtcMainnet22" +// Mainnet imUSD Contract +import { SavingsContractImusdMainnet22__factory } from "types/generated/factories/SavingsContractImusdMainnet22__factory" +import { SavingsContractImusdMainnet22 } from "types/generated/SavingsContractImusdMainnet22" + +import { deployContract } from "./utils/deploy-utils" +import { getSigner } from "./utils/signerFactory" +import { getChain, resolveAddress, getChainAddress } from "./utils/networkAddressFactory" +import { Chain } from "./utils/tokens" +import { verifyEtherscan } from "./utils/etherscan" + +task("upgrade-imusd-polygon", "Upgrade Polygon imUSD save contract imUSD") + .addOptionalParam("speed", "Defender Relayer speed param: 'safeLow' | 'average' | 'fast' | 'fastest'", "fast", types.string) + .setAction(async (taskArgs, hre) => { + const signer = await getSigner(hre, taskArgs.speed) + const chain = getChain(hre) + + if (chain !== Chain.polygon) throw Error("Task can only run against polygon or a polygon fork") + + const musdAddress = resolveAddress("mUSD", chain) + const imusdAddress = resolveAddress("mUSD", chain, "savings") + const delayedAdminAddress = getChainAddress("DelayedProxyAdmin", chain) + const nexusAddress = getChainAddress("Nexus", chain) + const unwrapperAddress = getChainAddress("Unwrapper", chain) + const constructorArguments = [nexusAddress, musdAddress, unwrapperAddress] + + // Deploy step 1 - Save Vault + const saveContractImpl = await deployContract( + new SavingsContractImusdPolygon22__factory(signer), + "mStable: mUSD Savings Contract (imUSD)", + constructorArguments, + ) + await verifyEtherscan(hre, { + address: saveContractImpl.address, + contract: "contracts/legacy-upgraded/imusd-polygon-22.sol:SavingsContract_imusd_polygon_22", + constructorArguments, + }) + + // Deploy step 2 - Propose upgrade + // Update the Save Contract proxy to point to the new implementation using the delayed proxy admin + const delayedProxyAdmin = DelayedProxyAdmin__factory.connect(delayedAdminAddress, signer) + + // Update the proxy to point to the new implementation using the delayed proxy admin + const upgradeData = [] + const proposeUpgradeData = delayedProxyAdmin.interface.encodeFunctionData("proposeUpgrade", [ + imusdAddress, + saveContractImpl.address, + upgradeData, + ]) + console.log(`\ndelayedProxyAdmin.proposeUpgrade to ${delayedAdminAddress}, data:\n${proposeUpgradeData}`) + }) + +task("upgrade-imusd-mainnet", "Upgrade Mainnet imUSD save contract imUSD") + .addOptionalParam("speed", "Defender Relayer speed param: 'safeLow' | 'average' | 'fast' | 'fastest'", "fast", types.string) + .setAction(async (taskArgs, hre) => { + const signer = await getSigner(hre, taskArgs.speed) + const chain = getChain(hre) + + if (chain !== Chain.mainnet) throw Error("Task can only run against mainnet or a mainnet fork") + + const imusdAddress = resolveAddress("mUSD", chain, "savings") + const delayedAdminAddress = getChainAddress("DelayedProxyAdmin", chain) + const unwrapperAddress = getChainAddress("Unwrapper", chain) + const constructorArguments = [] + + // Deploy step 1 - Save Contract + const saveContractImpl = await deployContract( + new SavingsContractImusdMainnet22__factory(signer), + "mStable: mUSD Savings Contract (imUSD)", + constructorArguments, + ) + // Validate the unwrapper is set as constant on the save contract + if ((await saveContractImpl.unwrapper()) !== unwrapperAddress || unwrapperAddress === DEAD_ADDRESS) + throw Error("Unwrapper address not set on save contract") + await verifyEtherscan(hre, { + address: saveContractImpl.address, + contract: "contracts/legacy-upgraded/imusd-mainnet-22.sol:SavingsContract_imusd_mainnet_22", + constructorArguments, + }) + + // Deploy step 2 - Propose upgrade + // Update the Save Contract proxy to point to the new implementation using the delayed proxy admin + const delayedProxyAdmin = DelayedProxyAdmin__factory.connect(delayedAdminAddress, signer) + + // Update the proxy to point to the new implementation using the delayed proxy admin + const upgradeData = [] + const proposeUpgradeData = delayedProxyAdmin.interface.encodeFunctionData("proposeUpgrade", [ + imusdAddress, + saveContractImpl.address, + upgradeData, + ]) + console.log(`\ndelayedProxyAdmin.proposeUpgrade to ${delayedAdminAddress}, data:\n${proposeUpgradeData}`) + }) + +task("upgrade-imbtc-mainnet", "Upgrade Mainnet imBTC save contract imBTC") + .addOptionalParam("speed", "Defender Relayer speed param: 'safeLow' | 'average' | 'fast' | 'fastest'", "fast", types.string) + .setAction(async (taskArgs, hre) => { + const signer = await getSigner(hre, taskArgs.speed) + const chain = getChain(hre) + + if (chain !== Chain.mainnet) throw Error("Task can only run against mainnet or a mainnet fork") + + const mbtcAddress = resolveAddress("mBTC", chain) + const imbtcAddress = resolveAddress("mBTC", chain, "savings") + const delayedAdminAddress = getChainAddress("DelayedProxyAdmin", chain) + const nexusAddress = getChainAddress("Nexus", chain) + const unwrapperAddress = getChainAddress("Unwrapper", chain) + + const constructorArguments = [nexusAddress, mbtcAddress, unwrapperAddress] + + // Deploy step 1 - Save Contract + const saveContractImpl = await deployContract( + new SavingsContractImbtcMainnet22__factory(signer), + "mStable: mBTC Savings Contract (imBTC)", + constructorArguments, + ) + await verifyEtherscan(hre, { + address: saveContractImpl.address, + contract: "contracts/legacy-upgraded/imbtc-mainnet-22.sol:SavingsContract_imbtc_mainnet_22", + constructorArguments, + }) + + // Deploy step 2 - Propose upgrade + // Update the Save Contract proxy to point to the new implementation using the delayed proxy admin + const delayedProxyAdmin = DelayedProxyAdmin__factory.connect(delayedAdminAddress, signer) + + // Update the proxy to point to the new implementation using the delayed proxy admin + const upgradeData = [] + const proposeUpgradeData = delayedProxyAdmin.interface.encodeFunctionData("proposeUpgrade", [ + imbtcAddress, + saveContractImpl.address, + upgradeData, + ]) + console.log(`\ndelayedProxyAdmin.proposeUpgrade to ${delayedAdminAddress}, data:\n${proposeUpgradeData}`) + }) +module.exports = {} diff --git a/test-fork/savings/sc22-mainnet-upgrade.spec.ts b/test-fork/savings/sc22-mainnet-upgrade.spec.ts index c356485e..fdc3e892 100644 --- a/test-fork/savings/sc22-mainnet-upgrade.spec.ts +++ b/test-fork/savings/sc22-mainnet-upgrade.spec.ts @@ -1,15 +1,9 @@ -import { impersonate } from "@utils/fork" +import { impersonate, setBalance } from "@utils/fork" import { Signer, ContractFactory } from "ethers" import { expect } from "chai" -import { network } from "hardhat" +import { ethers, network } from "hardhat" import { deployContract } from "tasks/utils/deploy-utils" -// Mainnet imBTC Vault -import { BoostedSavingsVaultImbtcMainnet2__factory } from "types/generated/factories/BoostedSavingsVaultImbtcMainnet2__factory" - -// Mainnet imUSD Vault -import { BoostedSavingsVaultImusdMainnet2__factory } from "types/generated/factories/BoostedSavingsVaultImusdMainnet2__factory" - // Mainnet imBTC Contract import { SavingsContractImbtcMainnet22__factory } from "types/generated/factories/SavingsContractImbtcMainnet22__factory" import { SavingsContractImbtcMainnet22 } from "types/generated/SavingsContractImbtcMainnet22" @@ -17,17 +11,9 @@ import { SavingsContractImbtcMainnet22 } from "types/generated/SavingsContractIm import { SavingsContractImusdMainnet22__factory } from "types/generated/factories/SavingsContractImusdMainnet22__factory" import { SavingsContractImusdMainnet22 } from "types/generated/SavingsContractImusdMainnet22" -import { - DelayedProxyAdmin, - DelayedProxyAdmin__factory, - IERC20, - ERC20__factory, - IERC20__factory, - Unwrapper, - Unwrapper__factory, -} from "types/generated" - -import { assertBNClosePercent, Chain, DEAD_ADDRESS, ZERO_ADDRESS, simpleToExactAmount, safeInfinity } from "index" +import { DelayedProxyAdmin, DelayedProxyAdmin__factory, IERC20, IERC20__factory, Unwrapper, Unwrapper__factory } from "types/generated" + +import { assertBNClosePercent, Chain, ZERO_ADDRESS, simpleToExactAmount } from "index" import { BigNumber } from "@ethersproject/bignumber" import { getChainAddress, resolveAddress } from "tasks/utils/networkAddressFactory" import { upgradeContract } from "@utils/deploy" @@ -40,36 +26,27 @@ const unwrapperAddress = getChainAddress("Unwrapper", chain) const deployerAddress = "0x19F12C947D25Ff8a3b748829D8001cA09a28D46d" const imusdHolderAddress = "0xdA1fD36cfC50ED03ca4dd388858A78C904379fb3" -const musdHolderAddress = "0x8474ddbe98f5aa3179b3b3f5942d724afcdec9f6" const imbtcHolderAddress = "0x720366c95d26389471c52f854d43292157c03efd" -const vmusdHolderAddress = "0x0c2ef8a1b3bc00bf676053732f31a67ebba5bd81" -const vmbtcHolderAddress = "0x10d96b1fd46ce7ce092aa905274b8ed9d4585a6e" -const vhbtcmbtcHolderAddress = "0x10d96b1fd46ce7ce092aa905274b8ed9d4585a6e" const daiAddress = resolveAddress("DAI", Chain.mainnet) const alusdAddress = resolveAddress("alUSD", Chain.mainnet) const musdAddress = resolveAddress("mUSD", Chain.mainnet) const imusdAddress = resolveAddress("mUSD", Chain.mainnet, "savings") -const imusdVaultAddress = resolveAddress("mUSD", Chain.mainnet, "vault") const alusdFeederPool = resolveAddress("alUSD", Chain.mainnet, "feederPool") const mbtcAddress = resolveAddress("mBTC", Chain.mainnet) const imbtcAddress = resolveAddress("mBTC", Chain.mainnet, "savings") -const imbtcVaultAddress = resolveAddress("mBTC", Chain.mainnet, "vault") const wbtcAddress = resolveAddress("WBTC", Chain.mainnet) const hbtcAddress = resolveAddress("HBTC", Chain.mainnet) const hbtcFeederPool = resolveAddress("HBTC", Chain.mainnet, "feederPool") // DEPLOYMENT PIPELINE -// 1. Connects Unwrapper -// 2. Upgrade and check storage -// 2.2. SavingsContracts -// 3. Do some unwrapping -// 3.1. Directly to unwrapper -// 3.2. Via SavingsContracts -// 3.3. Via SavingsVaults -// 4. Do 4626 SavingsContracts upgrades -context("Unwrapper and Vault4626 upgrades", () => { +// 1. Upgrade and check storage +// 1.1. SavingsContracts +// 2. Do some unwrapping +// 2.1. Directly to unwrapper +// 2.2. Via SavingsContracts +// 3. Test ERC4626 on SavingsContracts +context("SavingContract Vault4626 upgrades", () => { let deployer: Signer - let musdHolder: Signer let unwrapper: Unwrapper let governor: Signer let delayedProxyAdmin: DelayedProxyAdmin @@ -147,17 +124,43 @@ context("Unwrapper and Vault4626 upgrades", () => { { forking: { jsonRpcUrl: process.env.NODE_URL, - // Apr-01-2022 11:10:20 AM +UTC - blockNumber: 14500000, + // Apr-17-2022 01:54:24 AM +UTC + blockNumber: 14600000, }, }, ], }) - musdHolder = await impersonate(musdHolderAddress) deployer = await impersonate(deployerAddress) governor = await impersonate(governorAddress) - delayedProxyAdmin = DelayedProxyAdmin__factory.connect(delayedProxyAdminAddress, governor) + unwrapper = await Unwrapper__factory.connect(unwrapperAddress, deployer) + + // Set underlying assets balance for testing + await setBalance( + imbtcHolderAddress, + mbtcAddress, + simpleToExactAmount(1000, 14), + "0x6cb417529ba9d523d90ee650ef76cc0b9eccfd19232ffb9510f634b1fa3ecfaf", + ) + await setBalance( + imusdHolderAddress, + musdAddress, + simpleToExactAmount(1000, 18), + "0xe5fabcd29e7e9410c7da27fc68f987954a0ad327fe34ba95056b7880fd70df35", + ) + // Set savings contract balance for testing + await setBalance( + imbtcHolderAddress, + imbtcAddress, + simpleToExactAmount(1000, 14), + "0x6cb417529ba9d523d90ee650ef76cc0b9eccfd19232ffb9510f634b1fa3ecfaf", + ) + await setBalance( + imusdHolderAddress, + imusdAddress, + simpleToExactAmount(1000, 18), + "0xe5fabcd29e7e9410c7da27fc68f987954a0ad327fe34ba95056b7880fd70df35", + ) }) it("Test connectivity", async () => { const startEther = await deployer.getBalance() @@ -166,13 +169,7 @@ context("Unwrapper and Vault4626 upgrades", () => { }) context("Stage 1", () => { - it("Connects the unwrapper proxy contract ", async () => { - unwrapper = await Unwrapper__factory.connect(unwrapperAddress, deployer) - }) - }) - - context("Stage 2", () => { - describe("2.2 Upgrading savings contracts", () => { + describe("1.1 Upgrading savings contracts", () => { it("Upgrades the imUSD contract", async () => { const musdSaveImpl = await deployContract( new SavingsContractImusdMainnet22__factory(deployer), @@ -189,8 +186,7 @@ context("Unwrapper and Vault4626 upgrades", () => { delayedProxyAdmin, upgradeData, ) - - expect(await saveContractProxy.unwrapper()).to.eq(unwrapper.address) + expect(await saveContractProxy.unwrapper(), "unwrapper").to.eq(unwrapper.address) expect(await delayedProxyAdmin.getProxyImplementation(imusdAddress)).eq(musdSaveImpl.address) expect(musdAddress).eq(await musdSaveImpl.underlying()) }) @@ -224,132 +220,36 @@ context("Unwrapper and Vault4626 upgrades", () => { }) }) - context("Stage 3", () => { - describe("3.1 Directly", () => { - it("Can call getIsBassetOut & it functions correctly", async () => { - const isCredit = true - expect(await unwrapper.callStatic.getIsBassetOut(musdAddress, !isCredit, daiAddress)).to.eq(true) - expect(await unwrapper.callStatic.getIsBassetOut(musdAddress, !isCredit, musdAddress)).to.eq(false) - expect(await unwrapper.callStatic.getIsBassetOut(musdAddress, !isCredit, alusdAddress)).to.eq(false) - expect(await unwrapper.callStatic.getIsBassetOut(mbtcAddress, !isCredit, wbtcAddress)).to.eq(true) - expect(await unwrapper.callStatic.getIsBassetOut(mbtcAddress, !isCredit, mbtcAddress)).to.eq(false) - expect(await unwrapper.callStatic.getIsBassetOut(mbtcAddress, !isCredit, hbtcAddress)).to.eq(false) - }) - - const validateAssetRedemption = async ( - config: { - router: string - input: string - output: string - amount: BigNumber - isCredit: boolean - }, - signer: Signer, - ) => { - // Get estimated output via getUnwrapOutput - const signerAddress = await signer.getAddress() - const isBassetOut = await unwrapper.callStatic.getIsBassetOut(config.input, false, config.output) - - const amountOut = await unwrapper.getUnwrapOutput( - isBassetOut, - config.router, - config.input, - config.isCredit, - config.output, - config.amount, - ) - expect(amountOut.toString().length).to.be.gte(18) - const minAmountOut = amountOut.mul(98).div(1e2) - - const newConfig = { - ...config, - minAmountOut, - beneficiary: signerAddress, - } + context("Stage 2 (regression)", () => { + describe("2.1 Via SavingsContracts", () => { + before("fund accounts", async () => { + const imusdHolder = await impersonate(imusdHolderAddress) + const imbtcHolder = await impersonate(imbtcHolderAddress) - // check balance before - const tokenOut = IERC20__factory.connect(config.output, signer) - const tokenBalanceBefore = await tokenOut.balanceOf(signerAddress) - - // approve musd for unwrapping - const tokenInput = IERC20__factory.connect(config.input, signer) - await tokenInput.approve(unwrapper.address, config.amount) - - // redeem to basset via unwrapAndSend - await unwrapper - .connect(signer) - .unwrapAndSend( - isBassetOut, - newConfig.router, - newConfig.input, - newConfig.output, - newConfig.amount, - newConfig.minAmountOut, - newConfig.beneficiary, - ) - - // check balance after - const tokenBalanceAfter = await tokenOut.balanceOf(signerAddress) - expect(tokenBalanceAfter, "Token balance has increased").to.be.gt(tokenBalanceBefore) - } - - it("Receives the correct output from getUnwrapOutput", async () => { - const config = { - router: musdAddress, - input: musdAddress, - output: daiAddress, - amount: simpleToExactAmount(1, 18), - isCredit: false, - } - const isBassetOut = await unwrapper.callStatic.getIsBassetOut(config.input, config.isCredit, config.output) - const output = await unwrapper.getUnwrapOutput( - isBassetOut, - config.router, - config.input, - config.isCredit, - config.output, - config.amount, - ) - expect(output.toString()).to.be.length(19) - }) + const savingsContractImusd = SavingsContractImusdMainnet22__factory.connect(imusdAddress, imusdHolder) + const savingsContractImbtc = SavingsContractImbtcMainnet22__factory.connect(imbtcAddress, imbtcHolder) - it("mUSD redeem to bAsset via unwrapAndSend", async () => { - const config = { - router: musdAddress, - input: musdAddress, - output: daiAddress, - amount: simpleToExactAmount(1, 18), - isCredit: false, - } + const musd = IERC20__factory.connect(musdAddress, imusdHolder) + const mbtc = IERC20__factory.connect(mbtcAddress, imbtcHolder) - await validateAssetRedemption(config, musdHolder) - }) + await musd.approve(savingsContractImusd.address, simpleToExactAmount(1, 21)) + await mbtc.approve(savingsContractImbtc.address, simpleToExactAmount(1, 18)) - it("mUSD redeem to fAsset via unwrapAndSend", async () => { - const config = { - router: alusdFeederPool, - input: musdAddress, - output: alusdAddress, - amount: simpleToExactAmount(1, 18), - isCredit: false, - } - await validateAssetRedemption(config, musdHolder) + await savingsContractImusd["deposit(uint256,address)"](simpleToExactAmount(100), imusdHolderAddress) + await savingsContractImbtc["deposit(uint256,address)"](simpleToExactAmount(10, 14), imbtcHolderAddress) }) - }) - - describe("3.2 Via SavingsContracts", () => { it("mUSD contract redeem to bAsset", async () => { await redeemAndUnwrap(imusdHolderAddress, musdAddress, "musd", daiAddress) }) - it("mUSD contract redeem to fAsset", async () => { + it.skip("mUSD contract redeem to fAsset", async () => { await redeemAndUnwrap(imusdHolderAddress, alusdFeederPool, "musd", alusdAddress) }) it("mBTC contract redeem to bAsset", async () => { await redeemAndUnwrap(imbtcHolderAddress, mbtcAddress, "mbtc", wbtcAddress) }) - it("mBTC contract redeem to fAsset", async () => { + it.skip("mBTC contract redeem to fAsset", async () => { await redeemAndUnwrap(imbtcHolderAddress, hbtcFeederPool, "mbtc", hbtcAddress) }) // credits @@ -368,83 +268,9 @@ context("Unwrapper and Vault4626 upgrades", () => { await redeemAndUnwrap(imbtcHolderAddress, hbtcFeederPool, "mbtc", hbtcAddress, true) }) }) - - describe("3.3 Via Vaults", () => { - const withdrawAndUnwrap = async (holderAddress: string, router: string, input: "musd" | "mbtc", outputAddress: string) => { - const isCredit = true - const holder = await impersonate(holderAddress) - const vaultAddress = input === "musd" ? imusdVaultAddress : imbtcVaultAddress - const inputAddress = input === "musd" ? imusdAddress : imbtcAddress - const isBassetOut = await unwrapper.callStatic.getIsBassetOut(inputAddress, isCredit, outputAddress) - - const config = { - router, - input: inputAddress, - output: outputAddress, - amount: simpleToExactAmount(input === "musd" ? 100 : 10, 18), - isCredit, - } - - // Get estimated output via getUnwrapOutput - const amountOut = await unwrapper.getUnwrapOutput( - isBassetOut, - config.router, - config.input, - config.isCredit, - config.output, - config.amount, - ) - expect(amountOut.toString().length).to.be.gte(input === "musd" ? 18 : 9) - const minAmountOut = amountOut.mul(98).div(1e2) - - const outContract = IERC20__factory.connect(config.output, holder) - const tokenBalanceBefore = await outContract.balanceOf(holderAddress) - - // withdraw and unwrap - const saveVault = - input === "musd" - ? BoostedSavingsVaultImusdMainnet2__factory.connect(vaultAddress, holder) - : BoostedSavingsVaultImbtcMainnet2__factory.connect(vaultAddress, holder) - await saveVault.withdrawAndUnwrap(config.amount, minAmountOut, config.output, holderAddress, config.router, isBassetOut) - - const tokenBalanceAfter = await outContract.balanceOf(holderAddress) - const tokenBalanceDifference = tokenBalanceAfter.sub(tokenBalanceBefore) - assertBNClosePercent(tokenBalanceDifference, amountOut, 0.001) - expect(tokenBalanceAfter, "Token balance has increased").to.be.gt(tokenBalanceBefore) - } - - it("imUSD Vault redeem to bAsset", async () => { - await withdrawAndUnwrap(vmusdHolderAddress, musdAddress, "musd", daiAddress) - }) - - it("imUSD Vault redeem to fAsset", async () => { - await withdrawAndUnwrap(vmusdHolderAddress, alusdFeederPool, "musd", alusdAddress) - }) - it("imBTC Vault redeem to bAsset", async () => { - await withdrawAndUnwrap(vmbtcHolderAddress, mbtcAddress, "mbtc", wbtcAddress) - }) - - it("imBTC Vault redeem to fAsset", async () => { - await withdrawAndUnwrap(vhbtcmbtcHolderAddress, hbtcFeederPool, "mbtc", hbtcAddress) - }) - - it("Emits referrer successfully", async () => { - const saveContractProxy = SavingsContractImusdMainnet22__factory.connect(imusdAddress, musdHolder) - const musdContractProxy = ERC20__factory.connect(musdAddress, musdHolder) - await musdContractProxy.approve(imusdAddress, simpleToExactAmount(100, 18)) - const tx = await saveContractProxy["depositSavings(uint256,address,address)"]( - simpleToExactAmount(1, 18), - musdHolderAddress, - DEAD_ADDRESS, - ) - await expect(tx) - .to.emit(saveContractProxy, "Referral") - .withArgs(DEAD_ADDRESS, "0x8474DdbE98F5aA3179B3B3F5942D724aFcdec9f6", simpleToExactAmount(1, 18)) - }) - }) }) - context("Stage 4 Savings Contract Vault4626", () => { + context("Stage 3 Savings Contract ERC4626", () => { const saveContracts = [ { name: "imusd", address: imusdAddress }, { name: "imbtc", address: imbtcAddress }, @@ -459,8 +285,20 @@ context("Unwrapper and Vault4626 upgrades", () => { let holder: Signer let anotherHolder: Signer let assetsAmount = simpleToExactAmount(10, 18) - let sharesAmount = simpleToExactAmount(100, 18) - + let sharesAmount: BigNumber + let sharesBalance: BigNumber + let assetsBalance: BigNumber + let underlyingSaveContractBalance: BigNumber + let anotherUnderlyingBalance: BigNumber + + async function getBalances() { + underlyingSaveContractBalance = await asset.balanceOf(ctxSaveContract.address) + anotherUnderlyingBalance = await asset.balanceOf(anotherHolderAddress) + + sharesBalance = await ctxSaveContract.balanceOf(holderAddress) + assetsBalance = await ctxSaveContract.convertToAssets(sharesBalance) + sharesAmount = await ctxSaveContract.convertToShares(assetsAmount) + } before(async () => { if (sc.name === "imusd") { holder = await impersonate(imusdHolderAddress) @@ -468,19 +306,20 @@ context("Unwrapper and Vault4626 upgrades", () => { ctxSaveContract = SavingsContractImusdMainnet22__factory.connect(sc.address, holder) assetAddress = musdAddress assetsAmount = simpleToExactAmount(1, 18) - sharesAmount = simpleToExactAmount(10, 18) } else { holder = await impersonate(imbtcHolderAddress) anotherHolder = await impersonate(imusdHolderAddress) ctxSaveContract = SavingsContractImbtcMainnet22__factory.connect(sc.address, holder) assetAddress = mbtcAddress assetsAmount = simpleToExactAmount(1, 14) - sharesAmount = simpleToExactAmount(10, 14) } holderAddress = await holder.getAddress() anotherHolderAddress = await anotherHolder.getAddress() asset = IERC20__factory.connect(assetAddress, holder) }) + beforeEach(async () => { + await getBalances() + }) describe(`SaveContract ${sc.name}`, async () => { it("should properly store valid arguments", async () => { expect(await ctxSaveContract.asset(), "asset").to.eq(assetAddress) @@ -488,23 +327,51 @@ context("Unwrapper and Vault4626 upgrades", () => { describe("deposit", async () => { it("should deposit assets to the vault", async () => { await asset.approve(ctxSaveContract.address, simpleToExactAmount(1, 21)) - const shares = await ctxSaveContract.previewDeposit(assetsAmount) + let shares = await ctxSaveContract.previewDeposit(assetsAmount) expect(await ctxSaveContract.maxDeposit(holderAddress), "max deposit").to.gte(assetsAmount) expect(await ctxSaveContract.maxMint(holderAddress), "max mint").to.gte(shares) - - expect(await ctxSaveContract.maxRedeem(holderAddress), "max redeem").to.eq(0) - expect(await ctxSaveContract.maxWithdraw(holderAddress), "max withdraw").to.eq(0) - expect(await ctxSaveContract.totalAssets(), "totalAssets").to.eq(0) + expect(await ctxSaveContract.maxRedeem(holderAddress), "max redeem").to.eq(sharesBalance) + expect(await ctxSaveContract.maxWithdraw(holderAddress), "max withdraw").to.eq(assetsBalance) + expect(await ctxSaveContract.totalAssets(), "totalAssets").to.eq(underlyingSaveContractBalance) expect(await ctxSaveContract.convertToShares(assetsAmount), "convertToShares").to.lte(shares) // Test const tx = await ctxSaveContract.connect(holder)["deposit(uint256,address)"](assetsAmount, holderAddress) + // Exchange rate update + shares = await ctxSaveContract.previewDeposit(assetsAmount) + // Verify events, storage change, balance, etc. await expect(tx).to.emit(ctxSaveContract, "Deposit").withArgs(holderAddress, holderAddress, assetsAmount, shares) - expect(await ctxSaveContract.maxRedeem(holderAddress), "max redeem").to.lte(shares) - expect(await ctxSaveContract.maxWithdraw(holderAddress), "max withdraw").to.lte(assetsAmount) - expect(await ctxSaveContract.totalAssets(), "totalAssets").to.eq(assetsAmount) + assertBNClosePercent(await ctxSaveContract.maxRedeem(holderAddress), sharesBalance.add(shares), 0.01) + assertBNClosePercent(await ctxSaveContract.maxWithdraw(holderAddress), assetsBalance.add(assetsAmount), 0.01) + assertBNClosePercent(await ctxSaveContract.totalAssets(), underlyingSaveContractBalance.add(assetsAmount), 0.1) + }) + it("should deposit assets with referral", async () => { + await asset.approve(ctxSaveContract.address, simpleToExactAmount(1, 21)) + let shares = await ctxSaveContract.previewDeposit(assetsAmount) + + expect(await ctxSaveContract.maxDeposit(holderAddress), "max deposit").to.gte(assetsAmount) + expect(await ctxSaveContract.maxMint(holderAddress), "max mint").to.gte(shares) + expect(await ctxSaveContract.maxRedeem(holderAddress), "max redeem").to.eq(sharesBalance) + expect(await ctxSaveContract.maxWithdraw(holderAddress), "max withdraw").to.eq(assetsBalance) + expect(await ctxSaveContract.totalAssets(), "totalAssets").to.eq(underlyingSaveContractBalance) + expect(await ctxSaveContract.convertToShares(assetsAmount), "convertToShares").to.lte(shares) + + // Test + const tx = await ctxSaveContract + .connect(holder) + ["deposit(uint256,address,address)"](assetsAmount, holderAddress, anotherHolderAddress) + + shares = await ctxSaveContract.previewDeposit(assetsAmount) + + // Verify events, storage change, balance, etc. + await expect(tx).to.emit(ctxSaveContract, "Deposit").withArgs(holderAddress, holderAddress, assetsAmount, shares) + await expect(tx).to.emit(ctxSaveContract, "Referral").withArgs(anotherHolderAddress, holderAddress, assetsAmount) + + assertBNClosePercent(await ctxSaveContract.maxRedeem(holderAddress), sharesBalance.add(shares), 0.01) + assertBNClosePercent(await ctxSaveContract.maxWithdraw(holderAddress), assetsBalance.add(assetsAmount), 0.01) + assertBNClosePercent(await ctxSaveContract.totalAssets(), underlyingSaveContractBalance.add(assetsAmount), 0.1) }) it("fails if deposits zero", async () => { await expect(ctxSaveContract.connect(deployer)["deposit(uint256,address)"](0, holderAddress)).to.be.revertedWith( @@ -527,20 +394,47 @@ context("Unwrapper and Vault4626 upgrades", () => { expect(await ctxSaveContract.maxDeposit(holderAddress), "max deposit").to.gte(assets) expect(await ctxSaveContract.maxMint(holderAddress), "max mint").to.gte(shares) - expect(await ctxSaveContract.maxRedeem(holderAddress), "max redeem").to.eq(0) - expect(await ctxSaveContract.maxWithdraw(holderAddress), "max withdraw").to.eq(0) - expect(await ctxSaveContract.totalAssets(), "totalAssets").to.eq(0) + expect(await ctxSaveContract.maxRedeem(holderAddress), "max redeem").to.eq(sharesBalance) + expect(await ctxSaveContract.maxWithdraw(holderAddress), "max withdraw").to.eq(assetsBalance) + expect(await ctxSaveContract.totalAssets(), "totalAssets").to.eq(underlyingSaveContractBalance) expect(await ctxSaveContract.convertToShares(assets), "convertToShares").to.lte(shares) - expect(await ctxSaveContract.convertToAssets(shares), "convertToShares").to.lte(assets) + expect(await ctxSaveContract.convertToAssets(shares), "convertToAssets").to.lte(assets) - const tx = await ctxSaveContract.connect(holder).mint(shares, holderAddress) + const tx = await ctxSaveContract.connect(holder)["mint(uint256,address)"](shares, holderAddress) // Verify events, storage change, balance, etc. await expect(tx).to.emit(ctxSaveContract, "Deposit").withArgs(holderAddress, holderAddress, assets, shares) - expect(await ctxSaveContract.maxRedeem(holderAddress), "max redeem").to.lte(shares) - expect(await ctxSaveContract.maxWithdraw(holderAddress), "max withdraw").to.lte(assets) - expect(await ctxSaveContract.totalAssets(), "totalAssets").to.eq(assets) + expect(await ctxSaveContract.maxRedeem(holderAddress), "max redeem").to.eq(sharesBalance.add(sharesAmount)) + expect(await ctxSaveContract.maxWithdraw(holderAddress), "max withdraw").to.eq(assetsBalance.add(assetsAmount)) + expect(await ctxSaveContract.totalAssets(), "totalAssets").to.eq(underlyingSaveContractBalance.add(assetsAmount)) + }) + it("should mint shares with referral", async () => { + await asset.approve(ctxSaveContract.address, simpleToExactAmount(1, 21)) + // const shares = sharesAmount + const assets = await ctxSaveContract.previewMint(sharesAmount) + const shares = await ctxSaveContract.previewDeposit(assetsAmount) + + expect(await ctxSaveContract.maxDeposit(holderAddress), "max deposit").to.gte(assets) + expect(await ctxSaveContract.maxMint(holderAddress), "max mint").to.gte(shares) + + expect(await ctxSaveContract.maxRedeem(holderAddress), "max redeem").to.eq(sharesBalance) + expect(await ctxSaveContract.maxWithdraw(holderAddress), "max withdraw").to.eq(assetsBalance) + expect(await ctxSaveContract.totalAssets(), "totalAssets").to.eq(underlyingSaveContractBalance) + + expect(await ctxSaveContract.convertToShares(assets), "convertToShares").to.lte(shares) + expect(await ctxSaveContract.convertToAssets(shares), "convertToAssets").to.lte(assets) + + const tx = await ctxSaveContract + .connect(holder) + ["mint(uint256,address,address)"](shares, holderAddress, anotherHolderAddress) + // Verify events, storage change, balance, etc. + await expect(tx).to.emit(ctxSaveContract, "Deposit").withArgs(holderAddress, holderAddress, assets, shares) + await expect(tx).to.emit(ctxSaveContract, "Referral").withArgs(anotherHolderAddress, holderAddress, assetsAmount) + + expect(await ctxSaveContract.maxRedeem(holderAddress), "max redeem").to.eq(sharesBalance.add(sharesAmount)) + expect(await ctxSaveContract.maxWithdraw(holderAddress), "max withdraw").to.eq(assetsBalance.add(assetsAmount)) + expect(await ctxSaveContract.totalAssets(), "totalAssets").to.eq(underlyingSaveContractBalance.add(assetsAmount)) }) it("fails if mint zero", async () => { await expect(ctxSaveContract.connect(deployer)["mint(uint256,address)"](0, holderAddress)).to.be.revertedWith( @@ -557,95 +451,106 @@ context("Unwrapper and Vault4626 upgrades", () => { it("from the vault, same caller, receiver and owner", async () => { await asset.approve(ctxSaveContract.address, simpleToExactAmount(1, 21)) - expect(await ctxSaveContract.maxWithdraw(holderAddress), "max withdraw").to.eq(0) - expect(await ctxSaveContract.totalAssets(), "totalAssets").to.eq(0) + expect(await ctxSaveContract.maxWithdraw(holderAddress), "max withdraw").to.eq(assetsBalance) + expect(await ctxSaveContract.totalAssets(), "totalAssets").to.eq(underlyingSaveContractBalance) await ctxSaveContract.connect(holder)["deposit(uint256,address)"](assetsAmount, holderAddress) - expect(await ctxSaveContract.maxWithdraw(holderAddress), "max withdraw").to.gt(0) - expect(await ctxSaveContract.totalAssets(), "totalAssets").to.gt(0) + assertBNClosePercent(await ctxSaveContract.maxWithdraw(holderAddress), assetsBalance.add(assetsAmount), 0.01) + expect(await ctxSaveContract.totalAssets(), "totalAssets").to.gte(underlyingSaveContractBalance.sub(assetsAmount)) const shares = await ctxSaveContract.previewWithdraw(assetsAmount) - expect(await ctxSaveContract.maxRedeem(holderAddress), "max redeem").to.eq(shares) + expect(await ctxSaveContract.maxRedeem(holderAddress), "max redeem").to.eq(sharesBalance.add(sharesAmount)) + await getBalances() // Test const tx = await ctxSaveContract.connect(holder).withdraw(assetsAmount, holderAddress, holderAddress) // Verify events, storage change, balance, etc. await expect(tx) .to.emit(ctxSaveContract, "Withdraw") .withArgs(holderAddress, holderAddress, holderAddress, assetsAmount, shares) - expect(await ctxSaveContract.maxRedeem(holderAddress), "max redeem").to.eq(0) - expect(await ctxSaveContract.maxWithdraw(holderAddress), "max withdraw").to.eq(0) - expect(await ctxSaveContract.totalAssets(), "totalAssets").to.eq(0) + expect(await ctxSaveContract.maxRedeem(holderAddress), "max redeem").to.eq(sharesBalance.sub(sharesAmount)) + expect(await ctxSaveContract.maxWithdraw(holderAddress), "max withdraw").to.eq(assetsBalance.sub(assetsAmount)) + expect(await ctxSaveContract.totalAssets(), "totalAssets").to.eq(underlyingSaveContractBalance.sub(assetsAmount)) }) it("from the vault, caller != receiver and caller = owner", async () => { // Alice deposits assets (owner), Alice withdraws assets (caller), Bob receives assets (receiver) await asset.connect(holder).approve(ctxSaveContract.address, simpleToExactAmount(1, 21)) - expect(await ctxSaveContract.maxWithdraw(holderAddress), "max withdraw").to.eq(0) - expect(await ctxSaveContract.totalAssets(), "totalAssets").to.eq(0) + expect(await ctxSaveContract.maxWithdraw(holderAddress), "max withdraw").to.eq(assetsBalance) + expect(await ctxSaveContract.totalAssets(), "totalAssets").to.eq(underlyingSaveContractBalance) await ctxSaveContract.connect(holder)["deposit(uint256,address)"](assetsAmount, holderAddress) - expect(await ctxSaveContract.maxWithdraw(holderAddress), "max withdraw").to.gt(0) - expect(await ctxSaveContract.totalAssets(), "totalAssets").to.gt(0) + expect(await ctxSaveContract.maxWithdraw(holderAddress), "max withdraw").to.eq(assetsBalance.add(assetsAmount)) + expect(await ctxSaveContract.totalAssets(), "totalAssets").to.eq(underlyingSaveContractBalance.add(assetsAmount)) const shares = await ctxSaveContract.previewWithdraw(assetsAmount) - expect(await ctxSaveContract.maxRedeem(holderAddress), "max redeem").to.eq(shares) + expect(await ctxSaveContract.maxRedeem(holderAddress), "max redeem").to.eq(sharesBalance.add(sharesAmount)) + await getBalances() // Test const tx = await ctxSaveContract.connect(holder).withdraw(assetsAmount, anotherHolderAddress, holderAddress) // Verify events, storage change, balance, etc. await expect(tx) .to.emit(ctxSaveContract, "Withdraw") .withArgs(holderAddress, anotherHolderAddress, holderAddress, assetsAmount, shares) - expect(await ctxSaveContract.maxRedeem(holderAddress), "max redeem").to.eq(0) - expect(await ctxSaveContract.maxWithdraw(holderAddress), "max withdraw").to.eq(0) - expect(await ctxSaveContract.totalAssets(), "totalAssets").to.eq(0) + expect(await asset.balanceOf(anotherHolderAddress), "another holder balance").to.eq( + anotherUnderlyingBalance.add(assetsAmount), + ) + expect(await ctxSaveContract.balanceOf(holderAddress), "holder balance").to.eq(sharesBalance.sub(sharesAmount)) + expect(await ctxSaveContract.totalAssets(), "totalAssets").to.eq(underlyingSaveContractBalance.sub(assetsAmount)) }) it("from the vault caller != owner, infinite approval", async () => { // Alice deposits assets (owner), Bob withdraws assets (caller), Bob receives assets (receiver) - await asset.connect(holder).approve(ctxSaveContract.address, simpleToExactAmount(1, 21)) - await ctxSaveContract.connect(holder).approve(anotherHolderAddress, safeInfinity) + await asset.connect(holder).approve(ctxSaveContract.address, ethers.constants.MaxUint256) + await ctxSaveContract.connect(holder).approve(anotherHolderAddress, ethers.constants.MaxUint256) - expect(await ctxSaveContract.maxWithdraw(holderAddress), "max withdraw").to.eq(0) - expect(await ctxSaveContract.totalAssets(), "totalAssets").to.eq(0) + expect(await ctxSaveContract.maxWithdraw(holderAddress), "max withdraw").to.eq(assetsBalance) + expect(await ctxSaveContract.totalAssets(), "totalAssets").to.eq(underlyingSaveContractBalance) await ctxSaveContract.connect(holder)["deposit(uint256,address)"](assetsAmount, holderAddress) - expect(await ctxSaveContract.maxWithdraw(holderAddress), "max withdraw").to.gt(0) - expect(await ctxSaveContract.totalAssets(), "totalAssets").to.gt(0) + expect(await ctxSaveContract.maxWithdraw(holderAddress), "max withdraw").to.eq(assetsBalance.add(assetsAmount)) + expect(await ctxSaveContract.totalAssets(), "totalAssets").to.eq(underlyingSaveContractBalance.add(assetsAmount)) const shares = await ctxSaveContract.previewWithdraw(assetsAmount) - expect(await ctxSaveContract.maxRedeem(holderAddress), "max redeem").to.eq(shares) + expect(await ctxSaveContract.maxRedeem(holderAddress), "max redeem").to.eq(sharesBalance.add(sharesAmount)) + await getBalances() // Test const tx = await ctxSaveContract.connect(anotherHolder).withdraw(assetsAmount, anotherHolderAddress, holderAddress) // Verify events, storage change, balance, etc. await expect(tx) .to.emit(ctxSaveContract, "Withdraw") .withArgs(anotherHolderAddress, anotherHolderAddress, holderAddress, assetsAmount, shares) - expect(await ctxSaveContract.maxRedeem(holderAddress), "max redeem").to.eq(0) - expect(await ctxSaveContract.maxWithdraw(holderAddress), "max withdraw").to.eq(0) - expect(await ctxSaveContract.totalAssets(), "totalAssets").to.eq(0) + + expect(await asset.balanceOf(anotherHolderAddress), "another holder balance").to.eq( + anotherUnderlyingBalance.add(assetsAmount), + ) + expect(await ctxSaveContract.balanceOf(holderAddress), "holder balance").to.eq(sharesBalance.sub(sharesAmount)) + expect(await ctxSaveContract.totalAssets(), "totalAssets").to.eq(underlyingSaveContractBalance.sub(assetsAmount)) }) it("from the vault, caller != receiver and caller != owner", async () => { // Alice deposits assets (owner), Bob withdraws assets (caller), Bob receives assets (receiver) await asset.connect(holder).approve(ctxSaveContract.address, simpleToExactAmount(1, 21)) await ctxSaveContract.connect(holder).approve(anotherHolderAddress, simpleToExactAmount(1, 21)) - expect(await ctxSaveContract.maxWithdraw(holderAddress), "max withdraw").to.eq(0) - expect(await ctxSaveContract.totalAssets(), "totalAssets").to.eq(0) + expect(await ctxSaveContract.maxWithdraw(holderAddress), "max withdraw").to.eq(assetsBalance) + expect(await ctxSaveContract.totalAssets(), "totalAssets").to.eq(underlyingSaveContractBalance) await ctxSaveContract.connect(holder)["deposit(uint256,address)"](assetsAmount, holderAddress) - expect(await ctxSaveContract.maxWithdraw(holderAddress), "max withdraw").to.gt(0) - expect(await ctxSaveContract.totalAssets(), "totalAssets").to.gt(0) + expect(await ctxSaveContract.maxWithdraw(holderAddress), "max withdraw").to.eq(assetsBalance.add(assetsAmount)) + expect(await ctxSaveContract.totalAssets(), "totalAssets").to.eq(underlyingSaveContractBalance.add(assetsAmount)) const shares = await ctxSaveContract.previewWithdraw(assetsAmount) - expect(await ctxSaveContract.maxRedeem(holderAddress), "max redeem").to.eq(shares) + expect(await ctxSaveContract.maxRedeem(holderAddress), "max redeem").to.eq(sharesBalance.add(sharesAmount)) + await getBalances() // Test const tx = await ctxSaveContract.connect(anotherHolder).withdraw(assetsAmount, anotherHolderAddress, holderAddress) // Verify events, storage change, balance, etc. await expect(tx) .to.emit(ctxSaveContract, "Withdraw") .withArgs(anotherHolderAddress, anotherHolderAddress, holderAddress, assetsAmount, shares) - expect(await ctxSaveContract.maxRedeem(holderAddress), "max redeem").to.eq(0) - expect(await ctxSaveContract.maxWithdraw(holderAddress), "max withdraw").to.eq(0) - expect(await ctxSaveContract.totalAssets(), "totalAssets").to.eq(0) + expect(await asset.balanceOf(anotherHolderAddress), "another holder balance").to.eq( + anotherUnderlyingBalance.add(assetsAmount), + ) + expect(await ctxSaveContract.balanceOf(holderAddress), "holder balance").to.eq(sharesBalance.sub(sharesAmount)) + expect(await ctxSaveContract.totalAssets(), "totalAssets").to.eq(underlyingSaveContractBalance.sub(assetsAmount)) }) it("fails if deposits zero", async () => { await expect(ctxSaveContract.connect(deployer).withdraw(0, holderAddress, holderAddress)).to.be.revertedWith( @@ -659,16 +564,15 @@ context("Unwrapper and Vault4626 upgrades", () => { }) it("fail if caller != owner and it has not allowance", async () => { // Alice deposits assets (owner), Bob withdraws assets (caller), Bob receives assets (receiver) - await asset.connect(holder).approve(ctxSaveContract.address, simpleToExactAmount(1, 21)) + await ctxSaveContract.connect(holder).approve(anotherHolderAddress, 0) - expect(await ctxSaveContract.maxWithdraw(holderAddress), "max withdraw").to.eq(0) - expect(await ctxSaveContract.totalAssets(), "totalAssets").to.eq(0) + expect(await ctxSaveContract.maxWithdraw(holderAddress), "max withdraw").to.eq(assetsBalance) + expect(await ctxSaveContract.totalAssets(), "totalAssets").to.eq(underlyingSaveContractBalance) await ctxSaveContract.connect(holder)["deposit(uint256,address)"](assetsAmount, holderAddress) - expect(await ctxSaveContract.maxWithdraw(holderAddress), "max withdraw").to.gt(0) - expect(await ctxSaveContract.totalAssets(), "totalAssets").to.gt(0) - const shares = await ctxSaveContract.previewWithdraw(assetsAmount) - expect(await ctxSaveContract.maxRedeem(holderAddress), "max redeem").to.eq(shares) + expect(await ctxSaveContract.maxWithdraw(holderAddress), "max withdraw").to.eq(assetsBalance.add(assetsAmount)) + expect(await ctxSaveContract.totalAssets(), "totalAssets").to.eq(underlyingSaveContractBalance.add(assetsAmount)) + expect(await ctxSaveContract.maxRedeem(holderAddress), "max redeem").to.eq(sharesBalance.add(sharesAmount)) // Test const tx = ctxSaveContract.connect(anotherHolder).withdraw(assetsAmount, anotherHolderAddress, holderAddress) @@ -681,102 +585,105 @@ context("Unwrapper and Vault4626 upgrades", () => { await asset.approve(ctxSaveContract.address, simpleToExactAmount(1, 21)) const assets = await ctxSaveContract.previewRedeem(sharesAmount) - expect(await ctxSaveContract.maxRedeem(holderAddress), "max maxRedeem").to.eq(0) - expect(await ctxSaveContract.totalAssets(), "totalAssets").to.eq(0) + expect(await ctxSaveContract.maxRedeem(holderAddress), "max redeem").to.eq(sharesBalance) + expect(await ctxSaveContract.totalAssets(), "totalAssets").to.eq(underlyingSaveContractBalance) await ctxSaveContract.connect(holder)["deposit(uint256,address)"](assets, holderAddress) - expect(await ctxSaveContract.maxRedeem(holderAddress), "max maxRedeem").to.gt(0) - expect(await ctxSaveContract.totalAssets(), "totalAssets").to.gt(0) - const shares = await ctxSaveContract.maxRedeem(holderAddress) + expect(await ctxSaveContract.maxRedeem(holderAddress), "max redeem").to.eq(sharesBalance.add(sharesAmount)) + expect(await ctxSaveContract.totalAssets(), "totalAssets").to.eq(underlyingSaveContractBalance.add(assetsAmount)) + + await getBalances() // Test const tx = await ctxSaveContract .connect(holder) - ["redeem(uint256,address,address)"](shares, holderAddress, holderAddress) + ["redeem(uint256,address,address)"](sharesAmount, holderAddress, holderAddress) // Verify events, storage change, balance, etc. await expect(tx) .to.emit(ctxSaveContract, "Withdraw") - .withArgs(holderAddress, holderAddress, holderAddress, assets, shares) - expect(await ctxSaveContract.maxRedeem(holderAddress), "max redeem").to.eq(0) - expect(await ctxSaveContract.maxWithdraw(holderAddress), "max withdraw").to.eq(0) - expect(await ctxSaveContract.totalAssets(), "totalAssets").to.eq(0) + .withArgs(holderAddress, holderAddress, holderAddress, assets, sharesAmount) + expect(await ctxSaveContract.maxRedeem(holderAddress), "max redeem").to.eq(sharesBalance.sub(sharesAmount)) + expect(await ctxSaveContract.maxWithdraw(holderAddress), "max withdraw").to.eq(assetsBalance.sub(assetsAmount)) + expect(await ctxSaveContract.totalAssets(), "totalAssets").to.eq(underlyingSaveContractBalance.sub(assetsAmount)) }) it("from the vault, caller != receiver and caller = owner", async () => { // Alice deposits assets (owner), Alice withdraws assets (caller), Bob receives assets (receiver) await asset.connect(holder).approve(ctxSaveContract.address, simpleToExactAmount(1, 21)) const assets = await ctxSaveContract.previewRedeem(sharesAmount) - expect(await ctxSaveContract.maxRedeem(holderAddress), "max withdraw").to.eq(0) - expect(await ctxSaveContract.totalAssets(), "totalAssets").to.eq(0) + expect(await ctxSaveContract.maxRedeem(holderAddress), "max redeem").to.eq(sharesBalance) + expect(await ctxSaveContract.totalAssets(), "totalAssets").to.eq(underlyingSaveContractBalance) await ctxSaveContract.connect(holder)["deposit(uint256,address)"](assetsAmount, holderAddress) - expect(await ctxSaveContract.maxWithdraw(holderAddress), "max withdraw").to.eq(assets) - expect(await ctxSaveContract.totalAssets(), "totalAssets").to.gt(0) - const shares = await ctxSaveContract.maxRedeem(holderAddress) + expect(await ctxSaveContract.totalAssets(), "totalAssets").to.eq(underlyingSaveContractBalance.add(assetsAmount)) + + await getBalances() // Test const tx = await ctxSaveContract .connect(holder) - ["redeem(uint256,address,address)"](shares, anotherHolderAddress, holderAddress) + ["redeem(uint256,address,address)"](sharesAmount, anotherHolderAddress, holderAddress) // Verify events, storage change, balance, etc. await expect(tx) .to.emit(ctxSaveContract, "Withdraw") - .withArgs(holderAddress, anotherHolderAddress, holderAddress, assets, shares) - expect(await ctxSaveContract.maxRedeem(holderAddress), "max redeem").to.eq(0) - expect(await ctxSaveContract.maxWithdraw(holderAddress), "max withdraw").to.eq(0) - expect(await ctxSaveContract.totalAssets(), "totalAssets").to.eq(0) + .withArgs(holderAddress, anotherHolderAddress, holderAddress, assets, sharesAmount) + expect(await ctxSaveContract.maxRedeem(holderAddress), "max redeem").to.eq(sharesBalance.sub(sharesAmount)) + expect(await ctxSaveContract.maxWithdraw(holderAddress), "max withdraw").to.eq(assetsBalance.sub(assetsAmount)) + expect(await ctxSaveContract.totalAssets(), "totalAssets").to.eq(underlyingSaveContractBalance.sub(assetsAmount)) }) it("from the vault caller != owner, infinite approval", async () => { // Alice deposits assets (owner), Bob withdraws assets (caller), Bob receives assets (receiver) await asset.connect(holder).approve(ctxSaveContract.address, simpleToExactAmount(1, 21)) - await ctxSaveContract.connect(holder).approve(anotherHolderAddress, safeInfinity) + await ctxSaveContract.connect(holder).approve(anotherHolderAddress, ethers.constants.MaxUint256) const assets = await ctxSaveContract.previewRedeem(sharesAmount) - expect(await ctxSaveContract.maxWithdraw(holderAddress), "max withdraw").to.eq(0) - expect(await ctxSaveContract.totalAssets(), "totalAssets").to.eq(0) + expect(await ctxSaveContract.maxWithdraw(holderAddress), "max withdraw").to.eq(assetsBalance) + expect(await ctxSaveContract.totalAssets(), "totalAssets").to.eq(underlyingSaveContractBalance) await ctxSaveContract.connect(holder)["deposit(uint256,address)"](assets, holderAddress) - expect(await ctxSaveContract.maxWithdraw(holderAddress), "max withdraw").to.gt(0) - expect(await ctxSaveContract.totalAssets(), "totalAssets").to.gt(0) - const shares = await ctxSaveContract.maxRedeem(holderAddress) + expect(await ctxSaveContract.maxWithdraw(holderAddress), "max withdraw").to.eq(assetsBalance.add(assetsAmount)) + expect(await ctxSaveContract.totalAssets(), "totalAssets").to.eq(underlyingSaveContractBalance.add(assetsAmount)) + await getBalances() // Test const tx = await ctxSaveContract .connect(anotherHolder) - ["redeem(uint256,address,address)"](shares, anotherHolderAddress, holderAddress) + ["redeem(uint256,address,address)"](sharesAmount, anotherHolderAddress, holderAddress) // Verify events, storage change, balance, etc. await expect(tx) .to.emit(ctxSaveContract, "Withdraw") - .withArgs(anotherHolderAddress, anotherHolderAddress, holderAddress, assets, shares) - expect(await ctxSaveContract.maxRedeem(holderAddress), "max redeem").to.eq(0) - expect(await ctxSaveContract.maxWithdraw(holderAddress), "max withdraw").to.eq(0) - expect(await ctxSaveContract.totalAssets(), "totalAssets").to.eq(0) + .withArgs(anotherHolderAddress, anotherHolderAddress, holderAddress, assets, sharesAmount) + expect(await asset.balanceOf(anotherHolderAddress), "another holder balance").to.eq( + anotherUnderlyingBalance.add(assetsAmount), + ) + expect(await ctxSaveContract.balanceOf(holderAddress), "holder balance").to.eq(sharesBalance.sub(sharesAmount)) + expect(await ctxSaveContract.totalAssets(), "totalAssets").to.eq(underlyingSaveContractBalance.sub(assetsAmount)) }) it("from the vault, caller != receiver and caller != owner", async () => { // Alice deposits assets (owner), Bob withdraws assets (caller), Bob receives assets (receiver) await asset.connect(holder).approve(ctxSaveContract.address, simpleToExactAmount(1, 21)) await ctxSaveContract.connect(holder).approve(anotherHolderAddress, simpleToExactAmount(1, 21)) - const assets = await ctxSaveContract.previewRedeem(sharesAmount) - expect(await ctxSaveContract.maxWithdraw(holderAddress), "max withdraw").to.eq(0) - expect(await ctxSaveContract.totalAssets(), "totalAssets").to.eq(0) + expect(await ctxSaveContract.maxWithdraw(holderAddress), "max withdraw").to.eq(assetsBalance) + expect(await ctxSaveContract.totalAssets(), "totalAssets").to.eq(underlyingSaveContractBalance) await ctxSaveContract.connect(holder)["deposit(uint256,address)"](assets, holderAddress) - expect(await ctxSaveContract.maxWithdraw(holderAddress), "max withdraw").to.gt(0) - expect(await ctxSaveContract.totalAssets(), "totalAssets").to.gt(0) - const shares = await ctxSaveContract.maxRedeem(holderAddress) + expect(await ctxSaveContract.maxWithdraw(holderAddress), "max withdraw").to.eq(assetsBalance.add(assetsAmount)) + expect(await ctxSaveContract.totalAssets(), "totalAssets").to.eq(underlyingSaveContractBalance.add(assetsAmount)) + await getBalances() // Test const tx = await ctxSaveContract .connect(anotherHolder) - ["redeem(uint256,address,address)"](shares, anotherHolderAddress, holderAddress) + ["redeem(uint256,address,address)"](sharesAmount, anotherHolderAddress, holderAddress) // Verify events, storage change, balance, etc. await expect(tx) .to.emit(ctxSaveContract, "Withdraw") - .withArgs(anotherHolderAddress, anotherHolderAddress, holderAddress, assets, shares) - expect(await ctxSaveContract.maxRedeem(holderAddress), "max redeem").to.eq(0) - expect(await ctxSaveContract.maxWithdraw(holderAddress), "max withdraw").to.eq(0) - expect(await ctxSaveContract.totalAssets(), "totalAssets").to.eq(0) + .withArgs(anotherHolderAddress, anotherHolderAddress, holderAddress, assets, sharesAmount) + + expect(await ctxSaveContract.maxRedeem(anotherHolderAddress), "max redeem").to.eq(0) + expect(await ctxSaveContract.maxWithdraw(anotherHolderAddress), "max withdraw").to.eq(0) + expect(await ctxSaveContract.totalAssets(), "totalAssets").to.eq(underlyingSaveContractBalance.sub(assetsAmount)) }) it("fails if deposits zero", async () => { await expect( @@ -793,6 +700,9 @@ context("Unwrapper and Vault4626 upgrades", () => { await asset.connect(holder).approve(ctxSaveContract.address, simpleToExactAmount(1, 21)) const assets = await ctxSaveContract.previewRedeem(sharesAmount) await ctxSaveContract.connect(holder)["deposit(uint256,address)"](assets, holderAddress) + + await ctxSaveContract.connect(holder).approve(anotherHolderAddress, 0) + expect(await ctxSaveContract.connect(holder).allowance(holderAddress, anotherHolderAddress), "allowance").to.eq(0) // Test const tx = ctxSaveContract .connect(anotherHolder) diff --git a/test-fork/savings/sc22-polygon-upgrade.spec.ts b/test-fork/savings/sc22-polygon-upgrade.spec.ts index 956c6c4e..e7d432f3 100644 --- a/test-fork/savings/sc22-polygon-upgrade.spec.ts +++ b/test-fork/savings/sc22-polygon-upgrade.spec.ts @@ -1,26 +1,16 @@ -import { impersonate } from "@utils/fork" +import { impersonate, setBalance } from "@utils/fork" import { Signer, ContractFactory } from "ethers" import { expect } from "chai" -import { network } from "hardhat" +import { ethers, network } from "hardhat" import { deployContract } from "tasks/utils/deploy-utils" -// Polygon imUSD Vault -import { StakingRewardsWithPlatformTokenImusdPolygon2__factory } from "types/generated/factories/StakingRewardsWithPlatformTokenImusdPolygon2__factory" // Polygon imUSD Contract import { SavingsContractImusdPolygon22__factory } from "types/generated/factories/SavingsContractImusdPolygon22__factory" import { SavingsContractImusdPolygon22 } from "types/generated/SavingsContractImusdPolygon22" -import { - DelayedProxyAdmin, - DelayedProxyAdmin__factory, - ERC20__factory, - IERC20__factory, - Unwrapper, - Unwrapper__factory, - IERC20, -} from "types/generated" - -import { assertBNClosePercent, Chain, DEAD_ADDRESS, ZERO_ADDRESS, simpleToExactAmount, safeInfinity } from "index" +import { DelayedProxyAdmin, DelayedProxyAdmin__factory, IERC20__factory, Unwrapper, Unwrapper__factory, IERC20 } from "types/generated" + +import { assertBNClosePercent, Chain, ZERO_ADDRESS, simpleToExactAmount } from "index" import { BigNumber } from "@ethersproject/bignumber" import { getChainAddress, resolveAddress } from "tasks/utils/networkAddressFactory" import { upgradeContract } from "@utils/deploy" @@ -34,28 +24,22 @@ const unwrapperAddress = getChainAddress("Unwrapper", chain) const imusdHolderAddress = "0x9d8B7A637859668A903797D9f02DE2Aa05e5b0a0" const musdHolderAddress = "0xb14fFDB81E804D2792B6043B90aE5Ac973EcD53D" -const vmusdHolderAddress = "0x9d8B7A637859668A903797D9f02DE2Aa05e5b0a0" const daiAddress = resolveAddress("DAI", chain) const fraxAddress = resolveAddress("FRAX", chain) const musdAddress = resolveAddress("mUSD", chain) const imusdAddress = resolveAddress("mUSD", chain, "savings") -const imusdVaultAddress = resolveAddress("mUSD", chain, "vault") const fraxFeederPool = resolveAddress("FRAX", chain, "feederPool") // DEPLOYMENT PIPELINE -// 1. Connects Unwrapper -// 1.1. Set the Unwrapper address as constant in imUSD Vault via initialize -// 2. Upgrade and check storage -// 2.1. Vaults -// 2.2. SavingsContracts -// 3. Do some unwrapping -// 3.1. Directly to unwrapper -// 3.2. Via SavingsContracts -// 3.3. Via SavingsVaults -context("Unwrapper and Vault upgrades", () => { +// 1. Upgrade and check storage +// 1.1. SavingsContracts +// 2. Do some unwrapping +// 2.1. Directly to unwrapper +// 2.2. Via SavingsContracts +// 3. Test ERC4626 on SavingsContracts +context("SavingContract Vault4626 upgrades", () => { let deployer: Signer - let musdHolder: Signer let unwrapper: Unwrapper let governor: Signer let delayedProxyAdmin: DelayedProxyAdmin @@ -125,7 +109,6 @@ context("Unwrapper and Vault upgrades", () => { expect(tokenBalanceAfter, "Token balance has increased").to.be.gt(tokenBalanceBefore) expect(holderVaultBalanceAfter, "Vault balance has decreased").to.be.lt(holderVaultBalanceBefore) } - before("reset block number", async () => { await network.provider.request({ method: "hardhat_reset", @@ -133,17 +116,31 @@ context("Unwrapper and Vault upgrades", () => { { forking: { jsonRpcUrl: process.env.NODE_URL, - // Nov-25-2021 03:15:21 PM +UTC - blockNumber: 13684204, + // Apr-11-2022 07:42:51 AM +UTC + blockNumber: 27000000, }, }, ], }) - musdHolder = await impersonate(musdHolderAddress) deployer = await impersonate(deployerAddress) governor = await impersonate(governorAddress) delayedProxyAdmin = DelayedProxyAdmin__factory.connect(delayedProxyAdminAddress, governor) + unwrapper = await Unwrapper__factory.connect(unwrapperAddress, deployer) + // Set underlying assets balance for testing + await setBalance( + imusdHolderAddress, + musdAddress, + simpleToExactAmount(1000, 18), + "0xebdbf4f1890ca99983c2897c9302f3ab589eca3b34a6b11235c02075312ad1e4", + ) + // Set savings contract balance for testing + await setBalance( + imusdHolderAddress, + imusdAddress, + simpleToExactAmount(1000, 18), + "0xebdbf4f1890ca99983c2897c9302f3ab589eca3b34a6b11235c02075312ad1e4", + ) }) it("Test connectivity", async () => { const startEther = await deployer.getBalance() @@ -152,13 +149,7 @@ context("Unwrapper and Vault upgrades", () => { }) context("Stage 1", () => { - it("Connects the unwrapper proxy contract ", async () => { - unwrapper = await Unwrapper__factory.connect(unwrapperAddress, deployer) - }) - }) - - context("Stage 2", () => { - describe("2.2 Upgrading savings contracts", () => { + describe("1.1 Upgrading savings contracts", () => { it("Upgrades the imUSD contract", async () => { const constructorArguments = [nexusAddress, musdAddress, unwrapper.address] const musdSaveImpl = await deployContract( @@ -185,117 +176,19 @@ context("Unwrapper and Vault upgrades", () => { }) }) - context("Stage 3", () => { - describe("3.1 Directly", () => { - it("Can call getIsBassetOut & it functions correctly", async () => { - const isCredit = true - expect(await unwrapper.callStatic.getIsBassetOut(musdAddress, !isCredit, daiAddress)).to.eq(true) - expect(await unwrapper.callStatic.getIsBassetOut(musdAddress, !isCredit, musdAddress)).to.eq(false) - expect(await unwrapper.callStatic.getIsBassetOut(musdAddress, !isCredit, fraxAddress)).to.eq(false) - }) - - const validateAssetRedemption = async ( - config: { - router: string - input: string - output: string - amount: BigNumber - isCredit: boolean - }, - signer: Signer, - ) => { - // Get estimated output via getUnwrapOutput - const signerAddress = await signer.getAddress() - const isBassetOut = await unwrapper.callStatic.getIsBassetOut(config.input, false, config.output) - - const amountOut = await unwrapper.getUnwrapOutput( - isBassetOut, - config.router, - config.input, - config.isCredit, - config.output, - config.amount, - ) - expect(amountOut.toString().length).to.be.gte(18) - const minAmountOut = amountOut.mul(98).div(1e2) - - const newConfig = { - ...config, - minAmountOut, - beneficiary: signerAddress, - } - - // check balance before - const tokenOut = IERC20__factory.connect(config.output, signer) - const tokenBalanceBefore = await tokenOut.balanceOf(signerAddress) - - // approve musd for unwrapping - const tokenInput = IERC20__factory.connect(config.input, signer) - await tokenInput.approve(unwrapper.address, config.amount) - - // redeem to basset via unwrapAndSend - await unwrapper - .connect(signer) - .unwrapAndSend( - isBassetOut, - newConfig.router, - newConfig.input, - newConfig.output, - newConfig.amount, - newConfig.minAmountOut, - newConfig.beneficiary, - ) - - // check balance after - const tokenBalanceAfter = await tokenOut.balanceOf(signerAddress) - expect(tokenBalanceAfter, "Token balance has increased").to.be.gt(tokenBalanceBefore) - } + context("Stage 2 (regression)", () => { + describe("2.1 Via SavingsContracts", () => { + before("fund accounts", async () => { + const imusdHolder = await impersonate(imusdHolderAddress) - it("Receives the correct output from getUnwrapOutput", async () => { - const config = { - router: musdAddress, - input: musdAddress, - output: daiAddress, - amount: simpleToExactAmount(1, 18), - isCredit: false, - } - const isBassetOut = await unwrapper.callStatic.getIsBassetOut(config.input, config.isCredit, config.output) - const output = await unwrapper.getUnwrapOutput( - isBassetOut, - config.router, - config.input, - config.isCredit, - config.output, - config.amount, - ) - expect(output.toString()).to.be.length(18) - }) + const savingsContractImusd = SavingsContractImusdPolygon22__factory.connect(imusdAddress, imusdHolder) - it("mUSD redeem to bAsset via unwrapAndSend", async () => { - const config = { - router: musdAddress, - input: musdAddress, - output: daiAddress, - amount: simpleToExactAmount(1, 18), - isCredit: false, - } + const musd = IERC20__factory.connect(musdAddress, imusdHolder) - await validateAssetRedemption(config, musdHolder) - }) + await musd.approve(savingsContractImusd.address, simpleToExactAmount(1, 21)) - it("mUSD redeem to fAsset via unwrapAndSend", async () => { - const config = { - router: fraxFeederPool, - input: musdAddress, - output: fraxAddress, - amount: simpleToExactAmount(1, 18), - isCredit: false, - } - await validateAssetRedemption(config, musdHolder) + await savingsContractImusd["deposit(uint256,address)"](simpleToExactAmount(100), imusdHolderAddress) }) - }) - - describe("3.2 Via SavingsContracts", () => { it("mUSD contract redeem to bAsset", async () => { await redeemAndUnwrap(imusdHolderAddress, musdAddress, "musd", daiAddress) }) @@ -312,77 +205,8 @@ context("Unwrapper and Vault upgrades", () => { await redeemAndUnwrap(imusdHolderAddress, fraxFeederPool, "musd", fraxAddress, true) }) }) - - describe("3.3 Via Vaults", () => { - const withdrawAndUnwrap = async (holderAddress: string, router: string, input: "musd" | "mbtc", outputAddress: string) => { - if (input === "mbtc") throw new Error("mBTC not supported") - - const isCredit = true - const holder = await impersonate(holderAddress) - const vaultAddress = imusdVaultAddress - const inputAddress = imusdAddress - const isBassetOut = await unwrapper.callStatic.getIsBassetOut(inputAddress, isCredit, outputAddress) - const config = { - router, - input: inputAddress, - output: outputAddress, - amount: simpleToExactAmount(1, 18), - isCredit, - } - - // Get estimated output via getUnwrapOutput - const amountOut = await unwrapper.getUnwrapOutput( - isBassetOut, - config.router, - config.input, - config.isCredit, - config.output, - config.amount, - ) - expect(amountOut.toString().length).to.be.gte(input === "musd" ? 18 : 9) - const minAmountOut = amountOut.mul(98).div(1e2) - - const outContract = IERC20__factory.connect(config.output, holder) - const tokenBalanceBefore = await outContract.balanceOf(holderAddress) - - // withdraw and unwrap - const saveVault = StakingRewardsWithPlatformTokenImusdPolygon2__factory.connect(vaultAddress, holder) - const holderVaultBalanceBefore = await saveVault.balanceOf(holderAddress) - - await saveVault.withdrawAndUnwrap(config.amount, minAmountOut, config.output, holderAddress, config.router, isBassetOut) - const holderVaultBalanceAfter = await saveVault.balanceOf(holderAddress) - - const tokenBalanceAfter = await outContract.balanceOf(holderAddress) - const tokenBalanceDifference = tokenBalanceAfter.sub(tokenBalanceBefore) - assertBNClosePercent(tokenBalanceDifference, amountOut, 0.001) - expect(tokenBalanceAfter, "Token balance has increased").to.be.gt(tokenBalanceBefore) - expect(holderVaultBalanceAfter, "Vault balance has decreased").to.be.lt(holderVaultBalanceBefore) - } - - it("imUSD Vault redeem to bAsset", async () => { - await withdrawAndUnwrap(vmusdHolderAddress, musdAddress, "musd", daiAddress) - }) - - it("imUSD Vault redeem to fAsset", async () => { - await withdrawAndUnwrap(vmusdHolderAddress, fraxFeederPool, "musd", fraxAddress) - }) - - it("Emits referrer successfully", async () => { - const saveContractProxy = SavingsContractImusdPolygon22__factory.connect(imusdAddress, musdHolder) - const musdContractProxy = ERC20__factory.connect(musdAddress, musdHolder) - await musdContractProxy.approve(imusdAddress, simpleToExactAmount(100, 18)) - const tx = await saveContractProxy["depositSavings(uint256,address,address)"]( - simpleToExactAmount(1, 18), - musdHolderAddress, - DEAD_ADDRESS, - ) - await expect(tx) - .to.emit(saveContractProxy, "Referral") - .withArgs(DEAD_ADDRESS, musdHolderAddress, simpleToExactAmount(1, 18)) - }) - }) }) - context("Stage 4 Savings Contract Vault4626", () => { + context("Stage 3 Savings Contract ERC4626", () => { const saveContracts = [{ name: "imusd", address: imusdAddress }] saveContracts.forEach((sc) => { @@ -394,7 +218,20 @@ context("Unwrapper and Vault upgrades", () => { let holder: Signer let anotherHolder: Signer let assetsAmount = simpleToExactAmount(10, 18) - let sharesAmount = simpleToExactAmount(100, 18) + let sharesAmount: BigNumber + let sharesBalance: BigNumber + let assetsBalance: BigNumber + let underlyingSaveContractBalance: BigNumber + let anotherUnderlyingBalance: BigNumber + + async function getBalances() { + underlyingSaveContractBalance = await asset.balanceOf(ctxSaveContract.address) + anotherUnderlyingBalance = await asset.balanceOf(anotherHolderAddress) + + sharesBalance = await ctxSaveContract.balanceOf(holderAddress) + assetsBalance = await ctxSaveContract.convertToAssets(sharesBalance) + sharesAmount = await ctxSaveContract.convertToShares(assetsAmount) + } before(async () => { if (sc.name === "imusd") { @@ -403,7 +240,6 @@ context("Unwrapper and Vault upgrades", () => { ctxSaveContract = SavingsContractImusdPolygon22__factory.connect(sc.address, holder) assetAddress = musdAddress assetsAmount = simpleToExactAmount(1, 18) - sharesAmount = simpleToExactAmount(10, 18) } else { // not needed now. } @@ -411,6 +247,9 @@ context("Unwrapper and Vault upgrades", () => { anotherHolderAddress = await anotherHolder.getAddress() asset = IERC20__factory.connect(assetAddress, holder) }) + beforeEach(async () => { + await getBalances() + }) describe(`SaveContract ${sc.name}`, async () => { it("should properly store valid arguments", async () => { expect(await ctxSaveContract.asset(), "asset").to.eq(assetAddress) @@ -418,23 +257,50 @@ context("Unwrapper and Vault upgrades", () => { describe("deposit", async () => { it("should deposit assets to the vault", async () => { await asset.approve(ctxSaveContract.address, simpleToExactAmount(1, 21)) - const shares = await ctxSaveContract.previewDeposit(assetsAmount) + let shares = await ctxSaveContract.previewDeposit(assetsAmount) expect(await ctxSaveContract.maxDeposit(holderAddress), "max deposit").to.gte(assetsAmount) expect(await ctxSaveContract.maxMint(holderAddress), "max mint").to.gte(shares) - - expect(await ctxSaveContract.maxRedeem(holderAddress), "max redeem").to.eq(0) - expect(await ctxSaveContract.maxWithdraw(holderAddress), "max withdraw").to.eq(0) - expect(await ctxSaveContract.totalAssets(), "totalAssets").to.eq(0) + expect(await ctxSaveContract.maxRedeem(holderAddress), "max redeem").to.eq(sharesBalance) + expect(await ctxSaveContract.maxWithdraw(holderAddress), "max withdraw").to.eq(assetsBalance) + expect(await ctxSaveContract.totalAssets(), "totalAssets").to.eq(underlyingSaveContractBalance) expect(await ctxSaveContract.convertToShares(assetsAmount), "convertToShares").to.lte(shares) // Test const tx = await ctxSaveContract.connect(holder)["deposit(uint256,address)"](assetsAmount, holderAddress) + shares = await ctxSaveContract.previewDeposit(assetsAmount) + // Verify events, storage change, balance, etc. await expect(tx).to.emit(ctxSaveContract, "Deposit").withArgs(holderAddress, holderAddress, assetsAmount, shares) - expect(await ctxSaveContract.maxRedeem(holderAddress), "max redeem").to.lte(shares) - expect(await ctxSaveContract.maxWithdraw(holderAddress), "max withdraw").to.lte(assetsAmount) - expect(await ctxSaveContract.totalAssets(), "totalAssets").to.eq(assetsAmount) + assertBNClosePercent(await ctxSaveContract.maxRedeem(holderAddress), sharesBalance.add(shares), 0.01) + assertBNClosePercent(await ctxSaveContract.maxWithdraw(holderAddress), assetsBalance.add(assetsAmount), 0.01) + assertBNClosePercent(await ctxSaveContract.totalAssets(), underlyingSaveContractBalance.add(assetsAmount), 0.1) + }) + it("should deposit assets with referral", async () => { + await asset.approve(ctxSaveContract.address, simpleToExactAmount(1, 21)) + let shares = await ctxSaveContract.previewDeposit(assetsAmount) + + expect(await ctxSaveContract.maxDeposit(holderAddress), "max deposit").to.gte(assetsAmount) + expect(await ctxSaveContract.maxMint(holderAddress), "max mint").to.gte(shares) + expect(await ctxSaveContract.maxRedeem(holderAddress), "max redeem").to.eq(sharesBalance) + expect(await ctxSaveContract.maxWithdraw(holderAddress), "max withdraw").to.eq(assetsBalance) + expect(await ctxSaveContract.totalAssets(), "totalAssets").to.eq(underlyingSaveContractBalance) + expect(await ctxSaveContract.convertToShares(assetsAmount), "convertToShares").to.lte(shares) + + // Test + const tx = await ctxSaveContract + .connect(holder) + ["deposit(uint256,address,address)"](assetsAmount, holderAddress, anotherHolderAddress) + + shares = await ctxSaveContract.previewDeposit(assetsAmount) + + // Verify events, storage change, balance, etc. + await expect(tx).to.emit(ctxSaveContract, "Deposit").withArgs(holderAddress, holderAddress, assetsAmount, shares) + await expect(tx).to.emit(ctxSaveContract, "Referral").withArgs(anotherHolderAddress, holderAddress, assetsAmount) + + assertBNClosePercent(await ctxSaveContract.maxRedeem(holderAddress), sharesBalance.add(shares), 0.01) + assertBNClosePercent(await ctxSaveContract.maxWithdraw(holderAddress), assetsBalance.add(assetsAmount), 0.01) + assertBNClosePercent(await ctxSaveContract.totalAssets(), underlyingSaveContractBalance.add(assetsAmount), 0.1) }) it("fails if deposits zero", async () => { await expect(ctxSaveContract.connect(deployer)["deposit(uint256,address)"](0, holderAddress)).to.be.revertedWith( @@ -457,20 +323,47 @@ context("Unwrapper and Vault upgrades", () => { expect(await ctxSaveContract.maxDeposit(holderAddress), "max deposit").to.gte(assets) expect(await ctxSaveContract.maxMint(holderAddress), "max mint").to.gte(shares) - expect(await ctxSaveContract.maxRedeem(holderAddress), "max redeem").to.eq(0) - expect(await ctxSaveContract.maxWithdraw(holderAddress), "max withdraw").to.eq(0) - expect(await ctxSaveContract.totalAssets(), "totalAssets").to.eq(0) + expect(await ctxSaveContract.maxRedeem(holderAddress), "max redeem").to.eq(sharesBalance) + expect(await ctxSaveContract.maxWithdraw(holderAddress), "max withdraw").to.eq(assetsBalance) + expect(await ctxSaveContract.totalAssets(), "totalAssets").to.eq(underlyingSaveContractBalance) + + expect(await ctxSaveContract.convertToShares(assets), "convertToShares").to.lte(shares) + expect(await ctxSaveContract.convertToAssets(shares), "convertToAssets").to.lte(assets) + + const tx = await ctxSaveContract.connect(holder)["mint(uint256,address)"](shares, holderAddress) + // Verify events, storage change, balance, etc. + await expect(tx).to.emit(ctxSaveContract, "Deposit").withArgs(holderAddress, holderAddress, assets, shares) + + expect(await ctxSaveContract.maxRedeem(holderAddress), "max redeem").to.eq(sharesBalance.add(sharesAmount)) + expect(await ctxSaveContract.maxWithdraw(holderAddress), "max withdraw").to.eq(assetsBalance.add(assetsAmount)) + expect(await ctxSaveContract.totalAssets(), "totalAssets").to.eq(underlyingSaveContractBalance.add(assetsAmount)) + }) + it("should mint shares with referral", async () => { + await asset.approve(ctxSaveContract.address, simpleToExactAmount(1, 21)) + // const shares = sharesAmount + const assets = await ctxSaveContract.previewMint(sharesAmount) + const shares = await ctxSaveContract.previewDeposit(assetsAmount) + + expect(await ctxSaveContract.maxDeposit(holderAddress), "max deposit").to.gte(assets) + expect(await ctxSaveContract.maxMint(holderAddress), "max mint").to.gte(shares) + + expect(await ctxSaveContract.maxRedeem(holderAddress), "max redeem").to.eq(sharesBalance) + expect(await ctxSaveContract.maxWithdraw(holderAddress), "max withdraw").to.eq(assetsBalance) + expect(await ctxSaveContract.totalAssets(), "totalAssets").to.eq(underlyingSaveContractBalance) expect(await ctxSaveContract.convertToShares(assets), "convertToShares").to.lte(shares) - expect(await ctxSaveContract.convertToAssets(shares), "convertToShares").to.lte(assets) + expect(await ctxSaveContract.convertToAssets(shares), "convertToAssets").to.lte(assets) - const tx = await ctxSaveContract.connect(holder).mint(shares, holderAddress) + const tx = await ctxSaveContract + .connect(holder) + ["mint(uint256,address,address)"](shares, holderAddress, anotherHolderAddress) // Verify events, storage change, balance, etc. await expect(tx).to.emit(ctxSaveContract, "Deposit").withArgs(holderAddress, holderAddress, assets, shares) + await expect(tx).to.emit(ctxSaveContract, "Referral").withArgs(anotherHolderAddress, holderAddress, assetsAmount) - expect(await ctxSaveContract.maxRedeem(holderAddress), "max redeem").to.lte(shares) - expect(await ctxSaveContract.maxWithdraw(holderAddress), "max withdraw").to.lte(assets) - expect(await ctxSaveContract.totalAssets(), "totalAssets").to.eq(assets) + expect(await ctxSaveContract.maxRedeem(holderAddress), "max redeem").to.eq(sharesBalance.add(sharesAmount)) + expect(await ctxSaveContract.maxWithdraw(holderAddress), "max withdraw").to.eq(assetsBalance.add(assetsAmount)) + expect(await ctxSaveContract.totalAssets(), "totalAssets").to.eq(underlyingSaveContractBalance.add(assetsAmount)) }) it("fails if mint zero", async () => { await expect(ctxSaveContract.connect(deployer)["mint(uint256,address)"](0, holderAddress)).to.be.revertedWith( @@ -487,95 +380,106 @@ context("Unwrapper and Vault upgrades", () => { it("from the vault, same caller, receiver and owner", async () => { await asset.approve(ctxSaveContract.address, simpleToExactAmount(1, 21)) - expect(await ctxSaveContract.maxWithdraw(holderAddress), "max withdraw").to.eq(0) - expect(await ctxSaveContract.totalAssets(), "totalAssets").to.eq(0) + expect(await ctxSaveContract.maxWithdraw(holderAddress), "max withdraw").to.eq(assetsBalance) + expect(await ctxSaveContract.totalAssets(), "totalAssets").to.eq(underlyingSaveContractBalance) await ctxSaveContract.connect(holder)["deposit(uint256,address)"](assetsAmount, holderAddress) - expect(await ctxSaveContract.maxWithdraw(holderAddress), "max withdraw").to.gt(0) - expect(await ctxSaveContract.totalAssets(), "totalAssets").to.gt(0) + expect(await ctxSaveContract.maxWithdraw(holderAddress), "max withdraw").to.eq(assetsBalance.add(assetsAmount)) + expect(await ctxSaveContract.totalAssets(), "totalAssets").to.gte(underlyingSaveContractBalance.sub(assetsAmount)) const shares = await ctxSaveContract.previewWithdraw(assetsAmount) - expect(await ctxSaveContract.maxRedeem(holderAddress), "max redeem").to.eq(shares) + expect(await ctxSaveContract.maxRedeem(holderAddress), "max redeem").to.eq(sharesBalance.add(sharesAmount)) + await getBalances() // Test const tx = await ctxSaveContract.connect(holder).withdraw(assetsAmount, holderAddress, holderAddress) // Verify events, storage change, balance, etc. await expect(tx) .to.emit(ctxSaveContract, "Withdraw") .withArgs(holderAddress, holderAddress, holderAddress, assetsAmount, shares) - expect(await ctxSaveContract.maxRedeem(holderAddress), "max redeem").to.eq(0) - expect(await ctxSaveContract.maxWithdraw(holderAddress), "max withdraw").to.eq(0) - expect(await ctxSaveContract.totalAssets(), "totalAssets").to.eq(0) + expect(await ctxSaveContract.maxRedeem(holderAddress), "max redeem").to.eq(sharesBalance.sub(sharesAmount)) + expect(await ctxSaveContract.maxWithdraw(holderAddress), "max withdraw").to.eq(assetsBalance.sub(assetsAmount)) + expect(await ctxSaveContract.totalAssets(), "totalAssets").to.eq(underlyingSaveContractBalance.sub(assetsAmount)) }) it("from the vault, caller != receiver and caller = owner", async () => { // Alice deposits assets (owner), Alice withdraws assets (caller), Bob receives assets (receiver) await asset.connect(holder).approve(ctxSaveContract.address, simpleToExactAmount(1, 21)) - expect(await ctxSaveContract.maxWithdraw(holderAddress), "max withdraw").to.eq(0) - expect(await ctxSaveContract.totalAssets(), "totalAssets").to.eq(0) + expect(await ctxSaveContract.maxWithdraw(holderAddress), "max withdraw").to.eq(assetsBalance) + expect(await ctxSaveContract.totalAssets(), "totalAssets").to.eq(underlyingSaveContractBalance) await ctxSaveContract.connect(holder)["deposit(uint256,address)"](assetsAmount, holderAddress) - expect(await ctxSaveContract.maxWithdraw(holderAddress), "max withdraw").to.gt(0) - expect(await ctxSaveContract.totalAssets(), "totalAssets").to.gt(0) + expect(await ctxSaveContract.maxWithdraw(holderAddress), "max withdraw").to.eq(assetsBalance.add(assetsAmount)) + expect(await ctxSaveContract.totalAssets(), "totalAssets").to.eq(underlyingSaveContractBalance.add(assetsAmount)) const shares = await ctxSaveContract.previewWithdraw(assetsAmount) - expect(await ctxSaveContract.maxRedeem(holderAddress), "max redeem").to.eq(shares) + expect(await ctxSaveContract.maxRedeem(holderAddress), "max redeem").to.eq(sharesBalance.add(sharesAmount)) + await getBalances() // Test const tx = await ctxSaveContract.connect(holder).withdraw(assetsAmount, anotherHolderAddress, holderAddress) // Verify events, storage change, balance, etc. await expect(tx) .to.emit(ctxSaveContract, "Withdraw") .withArgs(holderAddress, anotherHolderAddress, holderAddress, assetsAmount, shares) - expect(await ctxSaveContract.maxRedeem(holderAddress), "max redeem").to.eq(0) - expect(await ctxSaveContract.maxWithdraw(holderAddress), "max withdraw").to.eq(0) - expect(await ctxSaveContract.totalAssets(), "totalAssets").to.eq(0) + expect(await asset.balanceOf(anotherHolderAddress), "another holder balance").to.eq( + anotherUnderlyingBalance.add(assetsAmount), + ) + expect(await ctxSaveContract.balanceOf(holderAddress), "holder balance").to.eq(sharesBalance.sub(sharesAmount)) + expect(await ctxSaveContract.totalAssets(), "totalAssets").to.eq(underlyingSaveContractBalance.sub(assetsAmount)) }) it("from the vault caller != owner, infinite approval", async () => { // Alice deposits assets (owner), Bob withdraws assets (caller), Bob receives assets (receiver) - await asset.connect(holder).approve(ctxSaveContract.address, simpleToExactAmount(1, 21)) - await ctxSaveContract.connect(holder).approve(anotherHolderAddress, safeInfinity) + await asset.connect(holder).approve(ctxSaveContract.address, ethers.constants.MaxUint256) + await ctxSaveContract.connect(holder).approve(anotherHolderAddress, ethers.constants.MaxUint256) - expect(await ctxSaveContract.maxWithdraw(holderAddress), "max withdraw").to.eq(0) - expect(await ctxSaveContract.totalAssets(), "totalAssets").to.eq(0) + expect(await ctxSaveContract.maxWithdraw(holderAddress), "max withdraw").to.eq(assetsBalance) + expect(await ctxSaveContract.totalAssets(), "totalAssets").to.eq(underlyingSaveContractBalance) await ctxSaveContract.connect(holder)["deposit(uint256,address)"](assetsAmount, holderAddress) - expect(await ctxSaveContract.maxWithdraw(holderAddress), "max withdraw").to.gt(0) - expect(await ctxSaveContract.totalAssets(), "totalAssets").to.gt(0) + expect(await ctxSaveContract.maxWithdraw(holderAddress), "max withdraw").to.eq(assetsBalance.add(assetsAmount)) + expect(await ctxSaveContract.totalAssets(), "totalAssets").to.eq(underlyingSaveContractBalance.add(assetsAmount)) const shares = await ctxSaveContract.previewWithdraw(assetsAmount) - expect(await ctxSaveContract.maxRedeem(holderAddress), "max redeem").to.eq(shares) + expect(await ctxSaveContract.maxRedeem(holderAddress), "max redeem").to.eq(sharesBalance.add(sharesAmount)) + await getBalances() // Test const tx = await ctxSaveContract.connect(anotherHolder).withdraw(assetsAmount, anotherHolderAddress, holderAddress) // Verify events, storage change, balance, etc. await expect(tx) .to.emit(ctxSaveContract, "Withdraw") .withArgs(anotherHolderAddress, anotherHolderAddress, holderAddress, assetsAmount, shares) - expect(await ctxSaveContract.maxRedeem(holderAddress), "max redeem").to.eq(0) - expect(await ctxSaveContract.maxWithdraw(holderAddress), "max withdraw").to.eq(0) - expect(await ctxSaveContract.totalAssets(), "totalAssets").to.eq(0) + + expect(await asset.balanceOf(anotherHolderAddress), "another holder balance").to.eq( + anotherUnderlyingBalance.add(assetsAmount), + ) + expect(await ctxSaveContract.balanceOf(holderAddress), "holder balance").to.eq(sharesBalance.sub(sharesAmount)) + expect(await ctxSaveContract.totalAssets(), "totalAssets").to.eq(underlyingSaveContractBalance.sub(assetsAmount)) }) it("from the vault, caller != receiver and caller != owner", async () => { // Alice deposits assets (owner), Bob withdraws assets (caller), Bob receives assets (receiver) await asset.connect(holder).approve(ctxSaveContract.address, simpleToExactAmount(1, 21)) await ctxSaveContract.connect(holder).approve(anotherHolderAddress, simpleToExactAmount(1, 21)) - expect(await ctxSaveContract.maxWithdraw(holderAddress), "max withdraw").to.eq(0) - expect(await ctxSaveContract.totalAssets(), "totalAssets").to.eq(0) + expect(await ctxSaveContract.maxWithdraw(holderAddress), "max withdraw").to.eq(assetsBalance) + expect(await ctxSaveContract.totalAssets(), "totalAssets").to.eq(underlyingSaveContractBalance) await ctxSaveContract.connect(holder)["deposit(uint256,address)"](assetsAmount, holderAddress) - expect(await ctxSaveContract.maxWithdraw(holderAddress), "max withdraw").to.gt(0) - expect(await ctxSaveContract.totalAssets(), "totalAssets").to.gt(0) + expect(await ctxSaveContract.maxWithdraw(holderAddress), "max withdraw").to.eq(assetsBalance.add(assetsAmount)) + expect(await ctxSaveContract.totalAssets(), "totalAssets").to.eq(underlyingSaveContractBalance.add(assetsAmount)) const shares = await ctxSaveContract.previewWithdraw(assetsAmount) - expect(await ctxSaveContract.maxRedeem(holderAddress), "max redeem").to.eq(shares) + expect(await ctxSaveContract.maxRedeem(holderAddress), "max redeem").to.eq(sharesBalance.add(sharesAmount)) + await getBalances() // Test const tx = await ctxSaveContract.connect(anotherHolder).withdraw(assetsAmount, anotherHolderAddress, holderAddress) // Verify events, storage change, balance, etc. await expect(tx) .to.emit(ctxSaveContract, "Withdraw") .withArgs(anotherHolderAddress, anotherHolderAddress, holderAddress, assetsAmount, shares) - expect(await ctxSaveContract.maxRedeem(holderAddress), "max redeem").to.eq(0) - expect(await ctxSaveContract.maxWithdraw(holderAddress), "max withdraw").to.eq(0) - expect(await ctxSaveContract.totalAssets(), "totalAssets").to.eq(0) + expect(await asset.balanceOf(anotherHolderAddress), "another holder balance").to.eq( + anotherUnderlyingBalance.add(assetsAmount), + ) + expect(await ctxSaveContract.balanceOf(holderAddress), "holder balance").to.eq(sharesBalance.sub(sharesAmount)) + expect(await ctxSaveContract.totalAssets(), "totalAssets").to.eq(underlyingSaveContractBalance.sub(assetsAmount)) }) it("fails if deposits zero", async () => { await expect(ctxSaveContract.connect(deployer).withdraw(0, holderAddress, holderAddress)).to.be.revertedWith( @@ -589,16 +493,15 @@ context("Unwrapper and Vault upgrades", () => { }) it("fail if caller != owner and it has not allowance", async () => { // Alice deposits assets (owner), Bob withdraws assets (caller), Bob receives assets (receiver) - await asset.connect(holder).approve(ctxSaveContract.address, simpleToExactAmount(1, 21)) + await ctxSaveContract.connect(holder).approve(anotherHolderAddress, 0) - expect(await ctxSaveContract.maxWithdraw(holderAddress), "max withdraw").to.eq(0) - expect(await ctxSaveContract.totalAssets(), "totalAssets").to.eq(0) + expect(await ctxSaveContract.maxWithdraw(holderAddress), "max withdraw").to.eq(assetsBalance) + expect(await ctxSaveContract.totalAssets(), "totalAssets").to.eq(underlyingSaveContractBalance) await ctxSaveContract.connect(holder)["deposit(uint256,address)"](assetsAmount, holderAddress) - expect(await ctxSaveContract.maxWithdraw(holderAddress), "max withdraw").to.gt(0) - expect(await ctxSaveContract.totalAssets(), "totalAssets").to.gt(0) - const shares = await ctxSaveContract.previewWithdraw(assetsAmount) - expect(await ctxSaveContract.maxRedeem(holderAddress), "max redeem").to.eq(shares) + expect(await ctxSaveContract.maxWithdraw(holderAddress), "max withdraw").to.eq(assetsBalance.add(assetsAmount)) + expect(await ctxSaveContract.totalAssets(), "totalAssets").to.eq(underlyingSaveContractBalance.add(assetsAmount)) + expect(await ctxSaveContract.maxRedeem(holderAddress), "max redeem").to.eq(sharesBalance.add(sharesAmount)) // Test const tx = ctxSaveContract.connect(anotherHolder).withdraw(assetsAmount, anotherHolderAddress, holderAddress) @@ -611,102 +514,105 @@ context("Unwrapper and Vault upgrades", () => { await asset.approve(ctxSaveContract.address, simpleToExactAmount(1, 21)) const assets = await ctxSaveContract.previewRedeem(sharesAmount) - expect(await ctxSaveContract.maxRedeem(holderAddress), "max maxRedeem").to.eq(0) - expect(await ctxSaveContract.totalAssets(), "totalAssets").to.eq(0) + expect(await ctxSaveContract.maxRedeem(holderAddress), "max redeem").to.eq(sharesBalance) + expect(await ctxSaveContract.totalAssets(), "totalAssets").to.eq(underlyingSaveContractBalance) await ctxSaveContract.connect(holder)["deposit(uint256,address)"](assets, holderAddress) - expect(await ctxSaveContract.maxRedeem(holderAddress), "max maxRedeem").to.gt(0) - expect(await ctxSaveContract.totalAssets(), "totalAssets").to.gt(0) - const shares = await ctxSaveContract.maxRedeem(holderAddress) + expect(await ctxSaveContract.maxRedeem(holderAddress), "max redeem").to.eq(sharesBalance.add(sharesAmount)) + expect(await ctxSaveContract.totalAssets(), "totalAssets").to.eq(underlyingSaveContractBalance.add(assetsAmount)) + + await getBalances() // Test const tx = await ctxSaveContract .connect(holder) - ["redeem(uint256,address,address)"](shares, holderAddress, holderAddress) + ["redeem(uint256,address,address)"](sharesAmount, holderAddress, holderAddress) // Verify events, storage change, balance, etc. await expect(tx) .to.emit(ctxSaveContract, "Withdraw") - .withArgs(holderAddress, holderAddress, holderAddress, assets, shares) - expect(await ctxSaveContract.maxRedeem(holderAddress), "max redeem").to.eq(0) - expect(await ctxSaveContract.maxWithdraw(holderAddress), "max withdraw").to.eq(0) - expect(await ctxSaveContract.totalAssets(), "totalAssets").to.eq(0) + .withArgs(holderAddress, holderAddress, holderAddress, assets, sharesAmount) + expect(await ctxSaveContract.maxRedeem(holderAddress), "max redeem").to.eq(sharesBalance.sub(sharesAmount)) + expect(await ctxSaveContract.maxWithdraw(holderAddress), "max withdraw").to.eq(assetsBalance.sub(assetsAmount)) + expect(await ctxSaveContract.totalAssets(), "totalAssets").to.eq(underlyingSaveContractBalance.sub(assetsAmount)) }) it("from the vault, caller != receiver and caller = owner", async () => { // Alice deposits assets (owner), Alice withdraws assets (caller), Bob receives assets (receiver) await asset.connect(holder).approve(ctxSaveContract.address, simpleToExactAmount(1, 21)) const assets = await ctxSaveContract.previewRedeem(sharesAmount) - expect(await ctxSaveContract.maxRedeem(holderAddress), "max withdraw").to.eq(0) - expect(await ctxSaveContract.totalAssets(), "totalAssets").to.eq(0) + expect(await ctxSaveContract.maxRedeem(holderAddress), "max redeem").to.eq(sharesBalance) + expect(await ctxSaveContract.totalAssets(), "totalAssets").to.eq(underlyingSaveContractBalance) await ctxSaveContract.connect(holder)["deposit(uint256,address)"](assetsAmount, holderAddress) - expect(await ctxSaveContract.maxWithdraw(holderAddress), "max withdraw").to.eq(assets) - expect(await ctxSaveContract.totalAssets(), "totalAssets").to.gt(0) - const shares = await ctxSaveContract.maxRedeem(holderAddress) + expect(await ctxSaveContract.totalAssets(), "totalAssets").to.eq(underlyingSaveContractBalance.add(assetsAmount)) + + await getBalances() // Test const tx = await ctxSaveContract .connect(holder) - ["redeem(uint256,address,address)"](shares, anotherHolderAddress, holderAddress) + ["redeem(uint256,address,address)"](sharesAmount, anotherHolderAddress, holderAddress) // Verify events, storage change, balance, etc. await expect(tx) .to.emit(ctxSaveContract, "Withdraw") - .withArgs(holderAddress, anotherHolderAddress, holderAddress, assets, shares) - expect(await ctxSaveContract.maxRedeem(holderAddress), "max redeem").to.eq(0) - expect(await ctxSaveContract.maxWithdraw(holderAddress), "max withdraw").to.eq(0) - expect(await ctxSaveContract.totalAssets(), "totalAssets").to.eq(0) + .withArgs(holderAddress, anotherHolderAddress, holderAddress, assets, sharesAmount) + expect(await ctxSaveContract.maxRedeem(holderAddress), "max redeem").to.eq(sharesBalance.sub(sharesAmount)) + expect(await ctxSaveContract.maxWithdraw(holderAddress), "max withdraw").to.eq(assetsBalance.sub(assetsAmount)) + expect(await ctxSaveContract.totalAssets(), "totalAssets").to.eq(underlyingSaveContractBalance.sub(assetsAmount)) }) it("from the vault caller != owner, infinite approval", async () => { // Alice deposits assets (owner), Bob withdraws assets (caller), Bob receives assets (receiver) await asset.connect(holder).approve(ctxSaveContract.address, simpleToExactAmount(1, 21)) - await ctxSaveContract.connect(holder).approve(anotherHolderAddress, safeInfinity) + await ctxSaveContract.connect(holder).approve(anotherHolderAddress, ethers.constants.MaxUint256) const assets = await ctxSaveContract.previewRedeem(sharesAmount) - expect(await ctxSaveContract.maxWithdraw(holderAddress), "max withdraw").to.eq(0) - expect(await ctxSaveContract.totalAssets(), "totalAssets").to.eq(0) + expect(await ctxSaveContract.maxWithdraw(holderAddress), "max withdraw").to.eq(assetsBalance) + expect(await ctxSaveContract.totalAssets(), "totalAssets").to.eq(underlyingSaveContractBalance) await ctxSaveContract.connect(holder)["deposit(uint256,address)"](assets, holderAddress) - expect(await ctxSaveContract.maxWithdraw(holderAddress), "max withdraw").to.gt(0) - expect(await ctxSaveContract.totalAssets(), "totalAssets").to.gt(0) - const shares = await ctxSaveContract.maxRedeem(holderAddress) + expect(await ctxSaveContract.maxWithdraw(holderAddress), "max withdraw").to.eq(assetsBalance.add(assetsAmount)) + expect(await ctxSaveContract.totalAssets(), "totalAssets").to.eq(underlyingSaveContractBalance.add(assetsAmount)) + await getBalances() // Test const tx = await ctxSaveContract .connect(anotherHolder) - ["redeem(uint256,address,address)"](shares, anotherHolderAddress, holderAddress) + ["redeem(uint256,address,address)"](sharesAmount, anotherHolderAddress, holderAddress) // Verify events, storage change, balance, etc. await expect(tx) .to.emit(ctxSaveContract, "Withdraw") - .withArgs(anotherHolderAddress, anotherHolderAddress, holderAddress, assets, shares) - expect(await ctxSaveContract.maxRedeem(holderAddress), "max redeem").to.eq(0) - expect(await ctxSaveContract.maxWithdraw(holderAddress), "max withdraw").to.eq(0) - expect(await ctxSaveContract.totalAssets(), "totalAssets").to.eq(0) + .withArgs(anotherHolderAddress, anotherHolderAddress, holderAddress, assets, sharesAmount) + expect(await asset.balanceOf(anotherHolderAddress), "another holder balance").to.eq( + anotherUnderlyingBalance.add(assetsAmount), + ) + expect(await ctxSaveContract.balanceOf(holderAddress), "holder balance").to.eq(sharesBalance.sub(sharesAmount)) + expect(await ctxSaveContract.totalAssets(), "totalAssets").to.eq(underlyingSaveContractBalance.sub(assetsAmount)) }) it("from the vault, caller != receiver and caller != owner", async () => { // Alice deposits assets (owner), Bob withdraws assets (caller), Bob receives assets (receiver) await asset.connect(holder).approve(ctxSaveContract.address, simpleToExactAmount(1, 21)) await ctxSaveContract.connect(holder).approve(anotherHolderAddress, simpleToExactAmount(1, 21)) - const assets = await ctxSaveContract.previewRedeem(sharesAmount) - expect(await ctxSaveContract.maxWithdraw(holderAddress), "max withdraw").to.eq(0) - expect(await ctxSaveContract.totalAssets(), "totalAssets").to.eq(0) + expect(await ctxSaveContract.maxWithdraw(holderAddress), "max withdraw").to.eq(assetsBalance) + expect(await ctxSaveContract.totalAssets(), "totalAssets").to.eq(underlyingSaveContractBalance) await ctxSaveContract.connect(holder)["deposit(uint256,address)"](assets, holderAddress) - expect(await ctxSaveContract.maxWithdraw(holderAddress), "max withdraw").to.gt(0) - expect(await ctxSaveContract.totalAssets(), "totalAssets").to.gt(0) - const shares = await ctxSaveContract.maxRedeem(holderAddress) + expect(await ctxSaveContract.maxWithdraw(holderAddress), "max withdraw").to.eq(assetsBalance.add(assetsAmount)) + expect(await ctxSaveContract.totalAssets(), "totalAssets").to.eq(underlyingSaveContractBalance.add(assetsAmount)) + await getBalances() // Test const tx = await ctxSaveContract .connect(anotherHolder) - ["redeem(uint256,address,address)"](shares, anotherHolderAddress, holderAddress) + ["redeem(uint256,address,address)"](sharesAmount, anotherHolderAddress, holderAddress) // Verify events, storage change, balance, etc. await expect(tx) .to.emit(ctxSaveContract, "Withdraw") - .withArgs(anotherHolderAddress, anotherHolderAddress, holderAddress, assets, shares) - expect(await ctxSaveContract.maxRedeem(holderAddress), "max redeem").to.eq(0) - expect(await ctxSaveContract.maxWithdraw(holderAddress), "max withdraw").to.eq(0) - expect(await ctxSaveContract.totalAssets(), "totalAssets").to.eq(0) + .withArgs(anotherHolderAddress, anotherHolderAddress, holderAddress, assets, sharesAmount) + + expect(await ctxSaveContract.maxRedeem(anotherHolderAddress), "max redeem").to.eq(0) + expect(await ctxSaveContract.maxWithdraw(anotherHolderAddress), "max withdraw").to.eq(0) + expect(await ctxSaveContract.totalAssets(), "totalAssets").to.eq(underlyingSaveContractBalance.sub(assetsAmount)) }) it("fails if deposits zero", async () => { await expect( @@ -723,6 +629,9 @@ context("Unwrapper and Vault upgrades", () => { await asset.connect(holder).approve(ctxSaveContract.address, simpleToExactAmount(1, 21)) const assets = await ctxSaveContract.previewRedeem(sharesAmount) await ctxSaveContract.connect(holder)["deposit(uint256,address)"](assets, holderAddress) + + await ctxSaveContract.connect(holder).approve(anotherHolderAddress, 0) + expect(await ctxSaveContract.connect(holder).allowance(holderAddress, anotherHolderAddress), "allowance").to.eq(0) // Test const tx = ctxSaveContract .connect(anotherHolder) diff --git a/test-utils/fork.ts b/test-utils/fork.ts index 97514d8d..1633b548 100644 --- a/test-utils/fork.ts +++ b/test-utils/fork.ts @@ -1,5 +1,7 @@ -import { Signer } from "ethers" +/* eslint-disable no-await-in-loop */ +import { Signer, utils } from "ethers" import { Account } from "types" +import { BN } from "./math" // impersonates a specific account export const impersonate = async (addr: string, fund = true): Promise => { @@ -30,3 +32,61 @@ export const impersonateAccount = async (address: string, fund = true): Promise< address, } } + +export const toBytes32 = (bn: BN): string => utils.hexlify(utils.zeroPad(bn.toHexString(), 32)) + +export const setStorageAt = async (address: string, index: string, value: string): Promise => { + const { ethers } = await import("hardhat") + + await ethers.provider.send("hardhat_setStorageAt", [address, index, value]) + await ethers.provider.send("evm_mine", []) // Just mines to the next block +} +/** + * + * Based on https://blog.euler.finance/brute-force-storage-layout-discovery-in-erc20-contracts-with-hardhat-7ff9342143ed + * @export + * @param {string} tokenAddress + * @return {*} {Promise} + */ +export const findBalancesSlot = async (tokenAddress: string): Promise => { + const { ethers, network } = await import("hardhat") + + const encode = (types, values) => ethers.utils.defaultAbiCoder.encode(types, values) + + const account = ethers.constants.AddressZero + const probeA = encode(["uint"], [1]) + const probeB = encode(["uint"], [2]) + const token = await ethers.getContractAt("@openzeppelin/contracts/token/ERC20/ERC20.sol:ERC20", tokenAddress) + + for (let i = 0; i < 100; i += 1) { + let probedSlot = ethers.utils.keccak256(encode(["address", "uint"], [account, i])) + // remove padding for JSON RPC + while (probedSlot.startsWith("0x0")) probedSlot = `0x${probedSlot.slice(3)}` + + const prev = await network.provider.send("eth_getStorageAt", [tokenAddress, probedSlot, "latest"]) + // make sure the probe will change the slot value + const probe = prev === probeA ? probeB : probeA + + await network.provider.send("hardhat_setStorageAt", [tokenAddress, probedSlot, probe]) + + const balance = await token.balanceOf(account) + // reset to previous value + await network.provider.send("hardhat_setStorageAt", [tokenAddress, probedSlot, prev]) + if (balance.eq(ethers.BigNumber.from(probe))) return i + } + throw new Error("Balances slot not found!") +} + +export const setBalance = async (userAddress: string, tokenAddress: string, amount: BN, slotIndex?: string): Promise => { + const balanceSlot = await findBalancesSlot(tokenAddress) + const index = + slotIndex === undefined + ? utils.solidityKeccak256( + ["uint256", "uint256"], + [userAddress, balanceSlot], // key, slot + ) + : slotIndex + + console.log(`Setting balance of user ${userAddress} with token ${tokenAddress} at index ${index}`) + await setStorageAt(tokenAddress, toBytes32(BN.from(index)), toBytes32(amount).toString()) +} diff --git a/test/savings/savings-contract.spec.ts b/test/savings/savings-contract.spec.ts index 150bb239..9a85b0d3 100644 --- a/test/savings/savings-contract.spec.ts +++ b/test/savings/savings-contract.spec.ts @@ -119,6 +119,7 @@ const mint4626Fn: ContractFnType = { name: "mint 4626", fn: "mint(uint256,address)", fnReceiver: "mint(uint256,address)", + fnReferrer: "mint(uint256,address,address)", event: "Deposit", } const redeemCreditsFn: ContractFnType = { @@ -720,7 +721,7 @@ describe("SavingsContract", async () => { "VM Exception", ) }) - it("should deposit the mUSD and assign credits to the saver", async () => { + it("should mint the imUSD and assign credits to the saver", async () => { const dataBefore = await getData(savingsContract, sa.default) let shares = simpleToExactAmount(10, 18) const assets = creditsToUnderlying(shares, initialExchangeRate) @@ -738,7 +739,7 @@ describe("SavingsContract", async () => { expect(dataAfter.balances.user).eq(dataBefore.balances.user.sub(assets)) expect(dataAfter.balances.contract).eq(assets) }) - it("allows alice to deposit to beneficiary (bob.address)", async () => { + it("allows alice to mint to beneficiary (bob.address)", async () => { const dataBefore = await getData(savingsContract, bob) let shares = simpleToExactAmount(10, 18) const assets = creditsToUnderlying(shares, initialExchangeRate) @@ -755,6 +756,23 @@ describe("SavingsContract", async () => { expect(dataAfter.balances.user).eq(dataBefore.balances.user) expect(dataAfter.balances.contract).eq(dataBefore.balances.contract.add(assets)) }) + it("allows alice to mint to beneficiary (bob.address) with a referral (charlie.address)", async () => { + const dataBefore = await getData(savingsContract, bob) + let shares = simpleToExactAmount(10, 18) + const assets = creditsToUnderlying(shares, initialExchangeRate) + // emulate decimals in the smart contract + shares = underlyingToCredits(assets, initialExchangeRate) + // emulate decimals in the smart contract + await masset.approve(savingsContract.address, assets) + + const tx = savingsContract.connect(alice.signer)[contractFn.fnReferrer](shares, bob.address, charlie.address) + await expect(tx).to.emit(savingsContract, "Referral").withArgs(charlie.address, bob.address, assets) + await expectToEmitDepositEvent(tx, alice.address, bob.address, assets, shares) + const dataAfter = await getData(savingsContract, bob) + expect(dataAfter.balances.userCredits, "Must receive some savings credits").eq(dataBefore.balances.userCredits.add(shares)) + expect(dataAfter.balances.totalCredits).eq(dataBefore.balances.totalCredits.add(shares)) + expect(dataAfter.balances.contract).eq(dataBefore.balances.contract.add(assets)) + }) }) describe("redeeming", async () => { async function validateRedeems(contractFn: ContractFnType) { @@ -947,12 +965,12 @@ describe("SavingsContract", async () => { } } describe(`using ${contractFn.name}`, async () => { - const deposit = simpleToExactAmount(10, 18) + const assets = simpleToExactAmount(10, 18) const interest = simpleToExactAmount(10, 18) beforeEach(async () => { await createNewSavingsContract() await masset.approve(savingsContract.address, simpleToExactAmount(1, 21)) - await savingsContract.preDeposit(deposit, alice.address) + await savingsContract.preDeposit(assets, alice.address) }) afterEach(async () => { const data = await getData(savingsContract, alice) @@ -966,7 +984,7 @@ describe("SavingsContract", async () => { await expect(withdrawToSender(savingsContract.connect(sa.other.signer))(amt)).to.be.revertedWith("VM Exception") }) it("allows full redemption immediately after deposit", async () => { - await withdrawToSender(savingsContract)(deposit) + await withdrawToSender(savingsContract)(assets) const data = await getData(savingsContract, alice) expect(data.balances.userCredits).eq(BN.from(0)) }) @@ -990,13 +1008,12 @@ describe("SavingsContract", async () => { await masset.setAmountForCollectInterest(interest) const dataBefore = await getData(savingsContract, alice) - const expectedCredits = underlyingToCredits(deposit, dataBefore.exchangeRate) - const tx = await withdrawToSender(savingsContract)(deposit) - // console.log("===expectToEmitWithdrawEvent==",alice.address, deposit.toString(), expectedCredits.toString() ) - // await expectToEmitWithdrawEvent(tx, alice.address, alice.address, deposit, expectedCredits) + const expectedCredits = underlyingToCredits(assets, expectedExchangeRate) + const tx = await withdrawToSender(savingsContract)(assets) + await expectToEmitWithdrawEvent(tx, alice.address, alice.address, assets, expectedCredits) const dataAfter = await getData(savingsContract, alice) - expect(dataAfter.balances.user).eq(dataBefore.balances.user.add(deposit)) + expect(dataAfter.balances.user).eq(dataBefore.balances.user.add(assets)) // User is left with resulting credits due to exchange rate going up assertBNClose(dataAfter.balances.userCredits, dataBefore.balances.userCredits.div(2), 1000) // Exchange rate updates @@ -1007,12 +1024,12 @@ describe("SavingsContract", async () => { await savingsContract.connect(sa.governor.signer).automateInterestCollectionFlag(false) const dataBefore = await getData(savingsContract, alice) - const tx = await withdrawToSender(savingsContract)(deposit) - const expectedCredits = underlyingToCredits(deposit, initialExchangeRate) - await expectToEmitWithdrawEvent(tx, alice.address, alice.address, deposit, expectedCredits) + const tx = await withdrawToSender(savingsContract)(assets) + const expectedCredits = underlyingToCredits(assets, initialExchangeRate) + await expectToEmitWithdrawEvent(tx, alice.address, alice.address, assets, expectedCredits) const dataAfter = await getData(savingsContract, alice) - expect(dataAfter.balances.user).eq(dataBefore.balances.user.add(deposit)) + expect(dataAfter.balances.user).eq(dataBefore.balances.user.add(assets)) expect(dataAfter.balances.userCredits).eq(BN.from(0)) expect(dataAfter.exchangeRate).eq(dataBefore.exchangeRate) }) diff --git a/test/shared/ERC4626.behaviour.ts b/test/shared/ERC4626.behaviour.ts index f2c87f5c..8cede53a 100644 --- a/test/shared/ERC4626.behaviour.ts +++ b/test/shared/ERC4626.behaviour.ts @@ -5,7 +5,6 @@ import { expect } from "chai" import { Account } from "types" import { ERC20, ERC205, IERC20Metadata, IERC4626Vault } from "types/generated" - export interface IERC4626BehaviourContext { vault: IERC4626Vault asset: ERC20 @@ -77,7 +76,7 @@ export function shouldBehaveLikeERC4626(ctx: IERC4626BehaviourContext): void { expect(await ctx.vault.convertToShares(assets), "convertToShares").to.lte(shares) expect(await ctx.vault.convertToAssets(shares), "convertToShares").to.lte(assets) - const tx = await ctx.vault.connect(alice.signer).mint(shares, alice.address) + const tx = await ctx.vault.connect(alice.signer)["mint(uint256,address)"](shares, alice.address) // Verify events, storage change, balance, etc. await expect(tx).to.emit(ctx.vault, "Deposit").withArgs(alice.address, alice.address, assets, shares) From 0ca5bf873d341f33fb9b3a17ce854a7791f6aa23 Mon Sep 17 00:00:00 2001 From: doncesarts Date: Wed, 20 Apr 2022 14:27:23 +0100 Subject: [PATCH 11/14] feat: adds ISavingsContractV4 --- contracts/interfaces/ISavingsContract.sol | 73 +++++++++++++++++++ .../legacy-upgraded/imbtc-mainnet-22.sol | 21 ++++-- .../legacy-upgraded/imusd-mainnet-22.sol | 17 ++++- .../legacy-upgraded/imusd-polygon-22.sol | 26 +++++-- .../boosted-staking/BoostedDualVault.sol | 4 +- .../rewards/boosted-staking/BoostedVault.sol | 4 +- contracts/rewards/staking/StakingRewards.sol | 4 +- .../StakingRewardsWithPlatformToken.sol | 4 +- contracts/savings/SavingsContract.sol | 15 +--- contracts/savings/peripheral/SaveWrapper.sol | 10 +-- contracts/savings/peripheral/Unwrapper.sol | 8 +- 11 files changed, 144 insertions(+), 42 deletions(-) diff --git a/contracts/interfaces/ISavingsContract.sol b/contracts/interfaces/ISavingsContract.sol index 77bd50c0..0f44c7a4 100644 --- a/contracts/interfaces/ISavingsContract.sol +++ b/contracts/interfaces/ISavingsContract.sol @@ -2,6 +2,7 @@ pragma solidity 0.8.6; import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import { IERC4626Vault } from "../interfaces/IERC4626Vault.sol"; interface ISavingsContractV1 { function depositInterest(uint256 _amount) external; @@ -104,3 +105,75 @@ interface ISavingsContractV3 { address _referrer ) external returns (uint256 creditsIssued); // V3 } + +interface ISavingsContractV4 is + IERC4626Vault // V4 +{ + // DEPRECATED but still backwards compatible + function redeem(uint256 _amount) external returns (uint256 massetReturned); // V1 (use IERC4626Vault.redeem) + + function creditBalances(address) external view returns (uint256); // V1 & V2 (use balanceOf) + + // -------------------------------------------- + function depositInterest(uint256 _amount) external; // V1 & V2 + + /** @dev see IERC4626Vault.deposit(uint256 assets, address receiver)*/ + function depositSavings(uint256 _amount) external returns (uint256 creditsIssued); // V1 & V2 + + /** @dev see IERC4626Vault.deposit(uint256 assets, address receiver)*/ + function depositSavings(uint256 _amount, address _beneficiary) + external + returns (uint256 creditsIssued); // V2 + + /** @dev see IERC4626Vault.redeem(uint256 shares,address receiver,address owner)(uint256 assets);*/ + function redeemCredits(uint256 _amount) external returns (uint256 underlyingReturned); // V2 + + /** @dev see IERC4626Vault.withdraw(uint256 assets,address receiver,address owner)(uint256 assets, address receiver)*/ + 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 + + /** @dev see IERC4626Vault.asset()(address assetTokenAddress);*/ + function underlying() external view returns (IERC20 underlyingMasset); // V2 + + function redeemAndUnwrap( + uint256 _amount, + bool _isCreditAmt, + uint256 _minAmountOut, + address _output, + address _beneficiary, + address _router, + bool _isBassetOut + ) + external + returns ( + uint256 creditsBurned, + uint256 massetRedeemed, + uint256 outputQuantity + ); // V3 + + function depositSavings( + uint256 _underlying, + address _beneficiary, + address _referrer + ) external returns (uint256 creditsIssued); // V3 + + // -------------------------------------------- V4 + function deposit( + uint256 assets, + address receiver, + address referrer + ) external returns (uint256 shares); + + function mint( + uint256 shares, + address receiver, + address referrer + ) external returns (uint256 assets); +} diff --git a/contracts/legacy-upgraded/imbtc-mainnet-22.sol b/contracts/legacy-upgraded/imbtc-mainnet-22.sol index a44a75d7..7c70cb0f 100644 --- a/contracts/legacy-upgraded/imbtc-mainnet-22.sol +++ b/contracts/legacy-upgraded/imbtc-mainnet-22.sol @@ -265,7 +265,7 @@ interface ISavingsManager { function collectAndDistributeInterest(address _mAsset) external; } -interface ISavingsContractV3 { +interface ISavingsContractV4 is IERC4626Vault { // DEPRECATED but still backwards compatible function redeem(uint256 _amount) external returns (uint256 massetReturned); @@ -316,6 +316,18 @@ interface ISavingsContractV3 { address _beneficiary, address _referrer ) external returns (uint256 creditsIssued); + + function deposit( + uint256 assets, + address receiver, + address referrer + ) external returns (uint256 shares); + + function mint( + uint256 shares, + address receiver, + address referrer + ) external returns (uint256 assets); } /* @@ -1076,8 +1088,7 @@ library StableMath { * DATE: 2022-04-08 */ contract SavingsContract_imbtc_mainnet_22 is - ISavingsContractV3, - IERC4626Vault, + ISavingsContractV4, Initializable, InitializableToken, ImmutableModule @@ -1886,7 +1897,7 @@ contract SavingsContract_imbtc_mainnet_22 is uint256 assets, address receiver, address referrer - ) external returns (uint256 shares) { + ) external override returns (uint256 shares) { shares = _transferAndMint(assets, receiver, true); emit Referral(referrer, receiver, assets); } @@ -1936,7 +1947,7 @@ contract SavingsContract_imbtc_mainnet_22 is uint256 shares, address receiver, address referrer - ) external returns (uint256 assets) { + ) external override returns (uint256 assets) { (assets, ) = _creditsToUnderlying(shares); _transferAndMint(assets, receiver, true); emit Referral(referrer, receiver, assets); diff --git a/contracts/legacy-upgraded/imusd-mainnet-22.sol b/contracts/legacy-upgraded/imusd-mainnet-22.sol index 5e272e98..0df7eec7 100644 --- a/contracts/legacy-upgraded/imusd-mainnet-22.sol +++ b/contracts/legacy-upgraded/imusd-mainnet-22.sol @@ -202,7 +202,7 @@ interface ISavingsContractV1 { function creditBalances(address) external view returns (uint256); } -interface ISavingsContractV3 { +interface ISavingsContractV4 { // DEPRECATED but still backwards compatible function redeem(uint256 _amount) external returns (uint256 massetReturned); @@ -253,6 +253,19 @@ interface ISavingsContractV3 { address _beneficiary, address _referrer ) external returns (uint256 creditsIssued); + + // -------------------------------------------- V4 + function deposit( + uint256 assets, + address receiver, + address referrer + ) external returns (uint256 shares); + + function mint( + uint256 shares, + address receiver, + address referrer + ) external returns (uint256 assets); } /* @@ -1273,7 +1286,7 @@ library StableMath { */ contract SavingsContract_imusd_mainnet_22 is ISavingsContractV1, - ISavingsContractV3, + ISavingsContractV4, IERC4626Vault, Initializable, InitializableToken, diff --git a/contracts/legacy-upgraded/imusd-polygon-22.sol b/contracts/legacy-upgraded/imusd-polygon-22.sol index 96d252d0..27a568b8 100644 --- a/contracts/legacy-upgraded/imusd-polygon-22.sol +++ b/contracts/legacy-upgraded/imusd-polygon-22.sol @@ -268,7 +268,9 @@ interface ISavingsManager { function lastBatchCollected(address _mAsset) external view returns (uint256); } -interface ISavingsContractV3 { +interface ISavingsContractV4 is + IERC4626Vault // V4 +{ // DEPRECATED but still backwards compatible function redeem(uint256 _amount) external returns (uint256 massetReturned); @@ -296,8 +298,6 @@ interface ISavingsContractV3 { function creditsToUnderlying(uint256 _underlying) external view returns (uint256 credits); // V2 - // -------------------------------------------- - function redeemAndUnwrap( uint256 _amount, bool _isCreditAmt, @@ -319,6 +319,19 @@ interface ISavingsContractV3 { address _beneficiary, address _referrer ) external returns (uint256 creditsIssued); + + // -------------------------------------------- V4 + function deposit( + uint256 assets, + address receiver, + address referrer + ) external returns (uint256 shares); + + function mint( + uint256 shares, + address receiver, + address referrer + ) external returns (uint256 assets); } /* @@ -1122,8 +1135,7 @@ library YieldValidator { * DATE: 2022-04-08 */ contract SavingsContract_imusd_polygon_22 is - ISavingsContractV3, - IERC4626Vault, + ISavingsContractV4, Initializable, InitializableToken, ImmutableModule @@ -1903,7 +1915,7 @@ contract SavingsContract_imusd_polygon_22 is uint256 assets, address receiver, address referrer - ) external returns (uint256 shares) { + ) external override returns (uint256 shares) { shares = _transferAndMint(assets, receiver, true); emit Referral(referrer, receiver, assets); } @@ -1953,7 +1965,7 @@ contract SavingsContract_imusd_polygon_22 is uint256 shares, address receiver, address referrer - ) external returns (uint256 assets) { + ) external override returns (uint256 assets) { (assets, ) = _creditsToUnderlying(shares); _transferAndMint(assets, receiver, true); emit Referral(referrer, receiver, assets); diff --git a/contracts/rewards/boosted-staking/BoostedDualVault.sol b/contracts/rewards/boosted-staking/BoostedDualVault.sol index 8b6cfa7b..c07f158e 100644 --- a/contracts/rewards/boosted-staking/BoostedDualVault.sol +++ b/contracts/rewards/boosted-staking/BoostedDualVault.sol @@ -4,7 +4,7 @@ pragma solidity 0.8.6; // Internal import { IRewardsRecipientWithPlatformToken } from "../../interfaces/IRewardsDistributionRecipient.sol"; import { IBoostedDualVaultWithLockup } from "../../interfaces/IBoostedDualVaultWithLockup.sol"; -import { ISavingsContractV3 } from "../../interfaces/ISavingsContract.sol"; +import { ISavingsContractV4 } from "../../interfaces/ISavingsContract.sol"; import { IRewardsDistributionRecipient, InitializableRewardsDistributionRecipient } from "../InitializableRewardsDistributionRecipient.sol"; import { BoostedTokenWrapper } from "./BoostedTokenWrapper.sol"; import { PlatformTokenVendor } from "../staking/PlatformTokenVendor.sol"; @@ -331,7 +331,7 @@ contract BoostedDualVault is _reduceRaw(_amount); // Unwrap `stakingToken` into `output` and send to `beneficiary` - (, , outputQuantity) = ISavingsContractV3(address(stakingToken)).redeemAndUnwrap( + (, , outputQuantity) = ISavingsContractV4(address(stakingToken)).redeemAndUnwrap( _amount, true, _minAmountOut, diff --git a/contracts/rewards/boosted-staking/BoostedVault.sol b/contracts/rewards/boosted-staking/BoostedVault.sol index 3be0fc28..30a82466 100644 --- a/contracts/rewards/boosted-staking/BoostedVault.sol +++ b/contracts/rewards/boosted-staking/BoostedVault.sol @@ -3,7 +3,7 @@ pragma solidity 0.8.6; // Internal import { IBoostedVaultWithLockup } from "../../interfaces/IBoostedVaultWithLockup.sol"; -import { ISavingsContractV3 } from "../../interfaces/ISavingsContract.sol"; +import { ISavingsContractV4 } from "../../interfaces/ISavingsContract.sol"; import { InitializableRewardsDistributionRecipient } from "../InitializableRewardsDistributionRecipient.sol"; import { BoostedTokenWrapper } from "./BoostedTokenWrapper.sol"; import { Initializable } from "../../shared/@openzeppelin-2.5/Initializable.sol"; @@ -287,7 +287,7 @@ contract BoostedVault is _reduceRaw(_amount); // Unwrap `stakingToken` into `output` and send to `beneficiary` - (, , outputQuantity) = ISavingsContractV3(address(stakingToken)).redeemAndUnwrap( + (, , outputQuantity) = ISavingsContractV4(address(stakingToken)).redeemAndUnwrap( _amount, true, _minAmountOut, diff --git a/contracts/rewards/staking/StakingRewards.sol b/contracts/rewards/staking/StakingRewards.sol index 65b62917..09c49812 100644 --- a/contracts/rewards/staking/StakingRewards.sol +++ b/contracts/rewards/staking/StakingRewards.sol @@ -4,7 +4,7 @@ pragma solidity 0.8.6; // Internal import { StakingTokenWrapper } from "./StakingTokenWrapper.sol"; import { InitializableRewardsDistributionRecipient } from "../InitializableRewardsDistributionRecipient.sol"; -import { ISavingsContractV3 } from "../../interfaces/ISavingsContract.sol"; +import { ISavingsContractV4 } from "../../interfaces/ISavingsContract.sol"; import { StableMath } from "../../shared/StableMath.sol"; // Libs @@ -203,7 +203,7 @@ contract StakingRewards is _reduceRaw(_amount); // Unwrap `stakingToken` into `output` and send to `beneficiary` - (, , outputQuantity) = ISavingsContractV3(address(stakingToken)).redeemAndUnwrap( + (, , outputQuantity) = ISavingsContractV4(address(stakingToken)).redeemAndUnwrap( _amount, true, _minAmountOut, diff --git a/contracts/rewards/staking/StakingRewardsWithPlatformToken.sol b/contracts/rewards/staking/StakingRewardsWithPlatformToken.sol index dec02705..b8024af0 100644 --- a/contracts/rewards/staking/StakingRewardsWithPlatformToken.sol +++ b/contracts/rewards/staking/StakingRewardsWithPlatformToken.sol @@ -5,7 +5,7 @@ pragma solidity 0.8.6; import { IRewardsDistributionRecipient, IRewardsRecipientWithPlatformToken } from "../../interfaces/IRewardsDistributionRecipient.sol"; import { IStakingRewardsWithPlatformToken } from "../../interfaces/IStakingRewardsWithPlatformToken.sol"; import { InitializableRewardsDistributionRecipient } from "../InitializableRewardsDistributionRecipient.sol"; -import { ISavingsContractV3 } from "../../interfaces/ISavingsContract.sol"; +import { ISavingsContractV4 } from "../../interfaces/ISavingsContract.sol"; import { StakingTokenWrapper } from "./StakingTokenWrapper.sol"; import { PlatformTokenVendor } from "./PlatformTokenVendor.sol"; import { StableMath } from "../../shared/StableMath.sol"; @@ -218,7 +218,7 @@ contract StakingRewardsWithPlatformToken is _reduceRaw(_amount); // Unwrap `stakingToken` into `output` and send to `beneficiary` - (, , outputQuantity) = ISavingsContractV3(address(stakingToken)).redeemAndUnwrap( + (, , outputQuantity) = ISavingsContractV4(address(stakingToken)).redeemAndUnwrap( _amount, true, _minAmountOut, diff --git a/contracts/savings/SavingsContract.sol b/contracts/savings/SavingsContract.sol index dadd431e..b012e7c5 100644 --- a/contracts/savings/SavingsContract.sol +++ b/contracts/savings/SavingsContract.sol @@ -5,9 +5,8 @@ pragma solidity 0.8.6; import { ISavingsManager } from "../interfaces/ISavingsManager.sol"; // Internal -import { ISavingsContractV3 } from "../interfaces/ISavingsContract.sol"; +import { ISavingsContractV4 } from "../interfaces/ISavingsContract.sol"; import { IUnwrapper } from "../interfaces/IUnwrapper.sol"; -import { IERC4626Vault } from "../interfaces/IERC4626Vault.sol"; import { InitializableToken } from "../shared/InitializableToken.sol"; import { ImmutableModule } from "../shared/ImmutableModule.sol"; import { IConnector } from "./peripheral/IConnector.sol"; @@ -27,13 +26,7 @@ import { YieldValidator } from "../shared/YieldValidator.sol"; * @dev VERSION: 2.2 * DATE: 2022-04-08 */ -contract SavingsContract is - ISavingsContractV3, - IERC4626Vault, - Initializable, - InitializableToken, - ImmutableModule -{ +contract SavingsContract is ISavingsContractV4, Initializable, InitializableToken, ImmutableModule { using StableMath for uint256; // Core events for depositing and withdrawing @@ -819,7 +812,7 @@ contract SavingsContract is uint256 assets, address receiver, address referrer - ) external returns (uint256 shares) { + ) external override returns (uint256 shares) { shares = _transferAndMint(assets, receiver, true); emit Referral(referrer, receiver, assets); } @@ -869,7 +862,7 @@ contract SavingsContract is uint256 shares, address receiver, address referrer - ) external returns (uint256 assets) { + ) external override returns (uint256 assets) { (assets, ) = _creditsToUnderlying(shares); _transferAndMint(assets, receiver, true); emit Referral(referrer, receiver, assets); diff --git a/contracts/savings/peripheral/SaveWrapper.sol b/contracts/savings/peripheral/SaveWrapper.sol index 7c914459..380cd572 100644 --- a/contracts/savings/peripheral/SaveWrapper.sol +++ b/contracts/savings/peripheral/SaveWrapper.sol @@ -7,7 +7,7 @@ import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.s import { IBoostedVaultWithLockup } from "../../interfaces/IBoostedVaultWithLockup.sol"; import { IFeederPool } from "../../interfaces/IFeederPool.sol"; import { IMasset } from "../../interfaces/IMasset.sol"; -import { ISavingsContractV3 } from "../../interfaces/ISavingsContract.sol"; +import { ISavingsContractV4 } from "../../interfaces/ISavingsContract.sol"; import { IUniswapV2Router02 } from "../../peripheral/Uniswap/IUniswapV2Router02.sol"; import { IBasicToken } from "../../shared/IBasicToken.sol"; import { ImmutableModule } from "../../shared/ImmutableModule.sol"; @@ -306,19 +306,19 @@ contract SaveWrapper is ImmutableModule { address _referrer ) internal { if (_stake && _referrer != address(0)) { - uint256 credits = ISavingsContractV3(_save).depositSavings( + uint256 credits = ISavingsContractV4(_save).depositSavings( _amount, address(this), _referrer ); IBoostedVaultWithLockup(_vault).stake(msg.sender, credits); } else if (_stake && _referrer == address(0)) { - uint256 credits = ISavingsContractV3(_save).depositSavings(_amount, address(this)); + uint256 credits = ISavingsContractV4(_save).depositSavings(_amount, address(this)); IBoostedVaultWithLockup(_vault).stake(msg.sender, credits); } else if (!_stake && _referrer != address(0)) { - ISavingsContractV3(_save).depositSavings(_amount, msg.sender, _referrer); + ISavingsContractV4(_save).depositSavings(_amount, msg.sender, _referrer); } else { - ISavingsContractV3(_save).depositSavings(_amount, msg.sender); + ISavingsContractV4(_save).depositSavings(_amount, msg.sender); } } diff --git a/contracts/savings/peripheral/Unwrapper.sol b/contracts/savings/peripheral/Unwrapper.sol index 42a86439..eea44b11 100644 --- a/contracts/savings/peripheral/Unwrapper.sol +++ b/contracts/savings/peripheral/Unwrapper.sol @@ -5,7 +5,7 @@ import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import { ImmutableModule } from "../../shared/ImmutableModule.sol"; -import { ISavingsContractV3 } from "../../interfaces/ISavingsContract.sol"; +import { ISavingsContractV4 } from "../../interfaces/ISavingsContract.sol"; import { IUnwrapper } from "../../interfaces/IUnwrapper.sol"; import { IMasset } from "../../interfaces/IMasset.sol"; import { IFeederPool } from "../../interfaces/IFeederPool.sol"; @@ -36,7 +36,7 @@ contract Unwrapper is IUnwrapper, ImmutableModule { bool _inputIsCredit, address _output ) external view override returns (bool isBassetOut) { - address input = _inputIsCredit ? address(ISavingsContractV3(_input).underlying()) : _input; + address input = _inputIsCredit ? address(ISavingsContractV4(_input).underlying()) : _input; (BassetPersonal[] memory bAssets, ) = IMasset(input).getBassets(); for (uint256 i = 0; i < bAssets.length; i++) { if (bAssets[i].addr == _output) return true; @@ -66,13 +66,13 @@ contract Unwrapper is IUnwrapper, ImmutableModule { uint256 _amount ) external view override returns (uint256 outputQuantity) { uint256 amt = _inputIsCredit - ? ISavingsContractV3(_input).creditsToUnderlying(_amount) + ? ISavingsContractV4(_input).creditsToUnderlying(_amount) : _amount; if (_isBassetOut) { outputQuantity = IMasset(_router).getRedeemOutput(_output, amt); } else { address input = _inputIsCredit - ? address(ISavingsContractV3(_input).underlying()) + ? address(ISavingsContractV4(_input).underlying()) : _input; outputQuantity = IFeederPool(_router).getSwapOutput(input, _output, amt); } From 4061d28ff213cd386696e34f1582695a0facb3dc Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Wed, 20 Apr 2022 13:59:24 +1000 Subject: [PATCH 12/14] chore: maxWithdraw and maxRedeem param was changed from caller to owner in the final spec. chore: Completed Natspec for some of the new ERC-4626 functions. --- .../legacy-upgraded/imbtc-mainnet-22.sol | 49 +++++++++++-------- .../legacy-upgraded/imusd-mainnet-22.sol | 49 +++++++++++-------- .../legacy-upgraded/imusd-polygon-22.sol | 49 +++++++++++-------- contracts/savings/SavingsContract.sol | 49 +++++++++++-------- 4 files changed, 116 insertions(+), 80 deletions(-) diff --git a/contracts/legacy-upgraded/imbtc-mainnet-22.sol b/contracts/legacy-upgraded/imbtc-mainnet-22.sol index 7c70cb0f..51ae3c47 100644 --- a/contracts/legacy-upgraded/imbtc-mainnet-22.sol +++ b/contracts/legacy-upgraded/imbtc-mainnet-22.sol @@ -1818,15 +1818,14 @@ contract SavingsContract_imbtc_mainnet_22 is /** * @notice it must be an ERC-20 token contract. Must not revert. * - * Returns the address of the underlying token used for the Vault uses for accounting, depositing, and withdrawing. + * @return assetTokenAddress the address of the underlying asset token. eg mUSD or mBTC */ function asset() external view override returns (address assetTokenAddress) { return address(underlying); } /** - * @notice The address of the underlying token used for the Vault uses for accounting, depositing, and withdrawing. - * Returns the total amount of the underlying asset that is “managed” by Vault. + * @return totalManagedAssets the total amount of the underlying asset tokens that is “managed” by Vault. */ function totalAssets() external view override returns (uint256 totalManagedAssets) { return underlying.balanceOf(address(this)); @@ -1925,10 +1924,11 @@ contract SavingsContract_imbtc_mainnet_22 is /** * @notice Mint exact amount of vault shares to the receiver by transferring enough underlying asset tokens from the caller. + * Emits a {Deposit} event. + * * @param shares The amount of vault shares to be minted. * @param receiver The account the vault shares will be minted to. * @return assets The amount of underlying assets that were transferred from the caller. - * Emits a {Deposit} event. */ function mint(uint256 shares, address receiver) external override returns (uint256 assets) { (assets, ) = _creditsToUnderlying(shares); @@ -1954,26 +1954,31 @@ contract SavingsContract_imbtc_mainnet_22 is } /** - * - * Returns Total number of underlying assets that caller can withdraw. + * @notice The maximum number of underlying assets that owner can withdraw. + * @param owner Address that owns the underlying assets. + * @return maxAssets The maximum amount of underlying assets the owner can withdraw. */ - function maxWithdraw(address caller) external view override returns (uint256 maxAssets) { - (maxAssets, ) = _creditsToUnderlying(balanceOf(caller)); + function maxWithdraw(address owner) external view override returns (uint256 maxAssets) { + (maxAssets, ) = _creditsToUnderlying(balanceOf(owner)); } /** * @notice Allows an on-chain or off-chain user to simulate the effects of their withdrawal at the current block, given current on-chain conditions. - * - * Return the exact amount of Vault shares that would be redeemed by the caller if withdrawing a given exact amount of underlying assets using the withdraw method. + * @param assets The amount of underlying assets to be withdrawn. + * @return shares The amount of vault shares that will be burnt. */ function previewWithdraw(uint256 assets) external view override returns (uint256 shares) { (shares, ) = _underlyingToCredits(assets); } /** - * Redeems shares from owner and sends assets of underlying tokens to receiver. - * Returns Total number of underlying shares redeemed. + * @notice Burns enough vault shares from owner and transfers the exact amount of underlying asset tokens to the receiver. * Emits a {Withdraw} event. + * + * @param assets The amount of underlying assets to be withdrawn from the vault. + * @param receiver The account that the underlying assets will be transferred to. + * @param owner Account that owns the vault shares to be burnt. + * @return shares The amount of vault shares that were burnt. */ function withdraw( uint256 assets, @@ -1991,18 +1996,19 @@ contract SavingsContract_imbtc_mainnet_22 is } /** - * @notice it must return a limited value if caller is subject to some withdrawal limit or timelock. must return balanceOf(caller) if caller is not subject to any withdrawal limit or timelock. MAY be used in the previewRedeem or redeem methods for shares input parameter. must NOT revert. + * @notice it must return a limited value if owner is subject to some withdrawal limit or timelock. must return balanceOf(owner) if owner is not subject to any withdrawal limit or timelock. MAY be used in the previewRedeem or redeem methods for shares input parameter. must NOT revert. * - * Returns Total number of underlying shares that caller can redeem. + * @param owner Address that owns the shares. + * @return maxShares Total number of shares that owner can redeem. */ - function maxRedeem(address caller) external view override returns (uint256 maxShares) { - maxShares = balanceOf(caller); + function maxRedeem(address owner) external view override returns (uint256 maxShares) { + maxShares = balanceOf(owner); } /** * @notice Allows an on-chain or off-chain user to simulate the effects of their redeemption at the current block, given current on-chain conditions. * - * Returns the exact amount of underlying assets that would be withdrawn by the caller if redeeming a given exact amount of Vault shares using the redeem method + * @return assets the exact amount of underlying assets that would be withdrawn by the caller if redeeming a given exact amount of Vault shares using the redeem method */ function previewRedeem(uint256 shares) external view override returns (uint256 assets) { (assets, ) = _creditsToUnderlying(shares); @@ -2010,10 +2016,13 @@ contract SavingsContract_imbtc_mainnet_22 is } /** - * Redeems shares from owner and sends assets of underlying tokens to receiver. - * - * Returns Total number of underlying assets of underlying redeemed. + * @notice Burns exact amount of vault shares from owner and transfers the underlying asset tokens to the receiver. * Emits a {Withdraw} event. + * + * @param shares The amount of vault shares to be burnt. + * @param receiver The account the underlying assets will be transferred to. + * @param owner The account that owns the vault shares to be burnt. + * @return assets The amount of underlying assets that were transferred to the receiver. */ function redeem( uint256 shares, diff --git a/contracts/legacy-upgraded/imusd-mainnet-22.sol b/contracts/legacy-upgraded/imusd-mainnet-22.sol index 0df7eec7..751628e3 100644 --- a/contracts/legacy-upgraded/imusd-mainnet-22.sol +++ b/contracts/legacy-upgraded/imusd-mainnet-22.sol @@ -1999,15 +1999,14 @@ contract SavingsContract_imusd_mainnet_22 is /** * @notice it must be an ERC-20 token contract. Must not revert. * - * Returns the address of the underlying token used for the Vault uses for accounting, depositing, and withdrawing. + * @return assetTokenAddress the address of the underlying asset token. eg mUSD or mBTC */ function asset() external view returns (address assetTokenAddress) { return address(underlying); } /** - * @notice The address of the underlying token used for the Vault uses for accounting, depositing, and withdrawing. - * Returns the total amount of the underlying asset that is “managed” by Vault. + * @return totalManagedAssets the total amount of the underlying asset tokens that is “managed” by Vault. */ function totalAssets() external view returns (uint256 totalManagedAssets) { return underlying.balanceOf(address(this)); @@ -2106,10 +2105,11 @@ contract SavingsContract_imusd_mainnet_22 is /** * @notice Mint exact amount of vault shares to the receiver by transferring enough underlying asset tokens from the caller. + * Emits a {Deposit} event. + * * @param shares The amount of vault shares to be minted. * @param receiver The account the vault shares will be minted to. * @return assets The amount of underlying assets that were transferred from the caller. - * Emits a {Deposit} event. */ function mint(uint256 shares, address receiver) external returns (uint256 assets) { (assets, ) = _creditsToUnderlying(shares); @@ -2135,26 +2135,31 @@ contract SavingsContract_imusd_mainnet_22 is } /** - * - * Returns Total number of underlying assets that caller can withdraw. + * @notice The maximum number of underlying assets that owner can withdraw. + * @param owner Address that owns the underlying assets. + * @return maxAssets The maximum amount of underlying assets the owner can withdraw. */ - function maxWithdraw(address caller) external view returns (uint256 maxAssets) { - (maxAssets, ) = _creditsToUnderlying(balanceOf(caller)); + function maxWithdraw(address owner) external view returns (uint256 maxAssets) { + (maxAssets, ) = _creditsToUnderlying(balanceOf(owner)); } /** * @notice Allows an on-chain or off-chain user to simulate the effects of their withdrawal at the current block, given current on-chain conditions. - * - * Return the exact amount of Vault shares that would be redeemed by the caller if withdrawing a given exact amount of underlying assets using the withdraw method. + * @param assets The amount of underlying assets to be withdrawn. + * @return shares The amount of vault shares that will be burnt. */ function previewWithdraw(uint256 assets) external view returns (uint256 shares) { (shares, ) = _underlyingToCredits(assets); } /** - * Redeems shares from owner and sends assets of underlying tokens to receiver. - * Returns Total number of underlying shares redeemed. + * @notice Burns enough vault shares from owner and transfers the exact amount of underlying asset tokens to the receiver. * Emits a {Withdraw} event. + * + * @param assets The amount of underlying assets to be withdrawn from the vault. + * @param receiver The account that the underlying assets will be transferred to. + * @param owner Account that owns the vault shares to be burnt. + * @return shares The amount of vault shares that were burnt. */ function withdraw( uint256 assets, @@ -2172,18 +2177,19 @@ contract SavingsContract_imusd_mainnet_22 is } /** - * @notice it must return a limited value if caller is subject to some withdrawal limit or timelock. must return balanceOf(caller) if caller is not subject to any withdrawal limit or timelock. MAY be used in the previewRedeem or redeem methods for shares input parameter. must NOT revert. + * @notice it must return a limited value if owner is subject to some withdrawal limit or timelock. must return balanceOf(owner) if owner is not subject to any withdrawal limit or timelock. MAY be used in the previewRedeem or redeem methods for shares input parameter. must NOT revert. * - * Returns Total number of underlying shares that caller can redeem. + * @param owner Address that owns the shares. + * @return maxShares Total number of shares that owner can redeem. */ - function maxRedeem(address caller) external view returns (uint256 maxShares) { - maxShares = balanceOf(caller); + function maxRedeem(address owner) external view returns (uint256 maxShares) { + maxShares = balanceOf(owner); } /** * @notice Allows an on-chain or off-chain user to simulate the effects of their redeemption at the current block, given current on-chain conditions. * - * Returns the exact amount of underlying assets that would be withdrawn by the caller if redeeming a given exact amount of Vault shares using the redeem method + * @return assets the exact amount of underlying assets that would be withdrawn by the caller if redeeming a given exact amount of Vault shares using the redeem method */ function previewRedeem(uint256 shares) external view returns (uint256 assets) { (assets, ) = _creditsToUnderlying(shares); @@ -2191,10 +2197,13 @@ contract SavingsContract_imusd_mainnet_22 is } /** - * Redeems shares from owner and sends assets of underlying tokens to receiver. - * - * Returns Total number of underlying assets of underlying redeemed. + * @notice Burns exact amount of vault shares from owner and transfers the underlying asset tokens to the receiver. * Emits a {Withdraw} event. + * + * @param shares The amount of vault shares to be burnt. + * @param receiver The account the underlying assets will be transferred to. + * @param owner The account that owns the vault shares to be burnt. + * @return assets The amount of underlying assets that were transferred to the receiver. */ function redeem( uint256 shares, diff --git a/contracts/legacy-upgraded/imusd-polygon-22.sol b/contracts/legacy-upgraded/imusd-polygon-22.sol index 27a568b8..49382885 100644 --- a/contracts/legacy-upgraded/imusd-polygon-22.sol +++ b/contracts/legacy-upgraded/imusd-polygon-22.sol @@ -1836,15 +1836,14 @@ contract SavingsContract_imusd_polygon_22 is /** * @notice it must be an ERC-20 token contract. Must not revert. * - * Returns the address of the underlying token used for the Vault uses for accounting, depositing, and withdrawing. + * @return assetTokenAddress the address of the underlying asset token. eg mUSD or mBTC */ function asset() external view override returns (address assetTokenAddress) { return address(underlying); } /** - * @notice The address of the underlying token used for the Vault uses for accounting, depositing, and withdrawing. - * Returns the total amount of the underlying asset that is “managed” by Vault. + * @return totalManagedAssets the total amount of the underlying asset tokens that is “managed” by Vault. */ function totalAssets() external view override returns (uint256 totalManagedAssets) { return underlying.balanceOf(address(this)); @@ -1943,10 +1942,11 @@ contract SavingsContract_imusd_polygon_22 is /** * @notice Mint exact amount of vault shares to the receiver by transferring enough underlying asset tokens from the caller. + * Emits a {Deposit} event. + * * @param shares The amount of vault shares to be minted. * @param receiver The account the vault shares will be minted to. * @return assets The amount of underlying assets that were transferred from the caller. - * Emits a {Deposit} event. */ function mint(uint256 shares, address receiver) external override returns (uint256 assets) { (assets, ) = _creditsToUnderlying(shares); @@ -1972,26 +1972,31 @@ contract SavingsContract_imusd_polygon_22 is } /** - * - * Returns Total number of underlying assets that caller can withdraw. + * @notice The maximum number of underlying assets that owner can withdraw. + * @param owner Address that owns the underlying assets. + * @return maxAssets The maximum amount of underlying assets the owner can withdraw. */ - function maxWithdraw(address caller) external view override returns (uint256 maxAssets) { - (maxAssets, ) = _creditsToUnderlying(balanceOf(caller)); + function maxWithdraw(address owner) external view override returns (uint256 maxAssets) { + (maxAssets, ) = _creditsToUnderlying(balanceOf(owner)); } /** * @notice Allows an on-chain or off-chain user to simulate the effects of their withdrawal at the current block, given current on-chain conditions. - * - * Return the exact amount of Vault shares that would be redeemed by the caller if withdrawing a given exact amount of underlying assets using the withdraw method. + * @param assets The amount of underlying assets to be withdrawn. + * @return shares The amount of vault shares that will be burnt. */ function previewWithdraw(uint256 assets) external view override returns (uint256 shares) { (shares, ) = _underlyingToCredits(assets); } /** - * Redeems shares from owner and sends assets of underlying tokens to receiver. - * Returns Total number of underlying shares redeemed. + * @notice Burns enough vault shares from owner and transfers the exact amount of underlying asset tokens to the receiver. * Emits a {Withdraw} event. + * + * @param assets The amount of underlying assets to be withdrawn from the vault. + * @param receiver The account that the underlying assets will be transferred to. + * @param owner Account that owns the vault shares to be burnt. + * @return shares The amount of vault shares that were burnt. */ function withdraw( uint256 assets, @@ -2009,18 +2014,19 @@ contract SavingsContract_imusd_polygon_22 is } /** - * @notice it must return a limited value if caller is subject to some withdrawal limit or timelock. must return balanceOf(caller) if caller is not subject to any withdrawal limit or timelock. MAY be used in the previewRedeem or redeem methods for shares input parameter. must NOT revert. + * @notice it must return a limited value if owner is subject to some withdrawal limit or timelock. must return balanceOf(owner) if owner is not subject to any withdrawal limit or timelock. MAY be used in the previewRedeem or redeem methods for shares input parameter. must NOT revert. * - * Returns Total number of underlying shares that caller can redeem. + * @param owner Address that owns the shares. + * @return maxShares Total number of shares that owner can redeem. */ - function maxRedeem(address caller) external view override returns (uint256 maxShares) { - maxShares = balanceOf(caller); + function maxRedeem(address owner) external view override returns (uint256 maxShares) { + maxShares = balanceOf(owner); } /** * @notice Allows an on-chain or off-chain user to simulate the effects of their redeemption at the current block, given current on-chain conditions. * - * Returns the exact amount of underlying assets that would be withdrawn by the caller if redeeming a given exact amount of Vault shares using the redeem method + * @return assets the exact amount of underlying assets that would be withdrawn by the caller if redeeming a given exact amount of Vault shares using the redeem method */ function previewRedeem(uint256 shares) external view override returns (uint256 assets) { (assets, ) = _creditsToUnderlying(shares); @@ -2028,10 +2034,13 @@ contract SavingsContract_imusd_polygon_22 is } /** - * Redeems shares from owner and sends assets of underlying tokens to receiver. - * - * Returns Total number of underlying assets of underlying redeemed. + * @notice Burns exact amount of vault shares from owner and transfers the underlying asset tokens to the receiver. * Emits a {Withdraw} event. + * + * @param shares The amount of vault shares to be burnt. + * @param receiver The account the underlying assets will be transferred to. + * @param owner The account that owns the vault shares to be burnt. + * @return assets The amount of underlying assets that were transferred to the receiver. */ function redeem( uint256 shares, diff --git a/contracts/savings/SavingsContract.sol b/contracts/savings/SavingsContract.sol index b012e7c5..dffc6808 100644 --- a/contracts/savings/SavingsContract.sol +++ b/contracts/savings/SavingsContract.sol @@ -733,15 +733,14 @@ contract SavingsContract is ISavingsContractV4, Initializable, InitializableToke /** * @notice it must be an ERC-20 token contract. Must not revert. * - * Returns the address of the underlying token used for the Vault uses for accounting, depositing, and withdrawing. + * @return assetTokenAddress the address of the underlying asset token. eg mUSD or mBTC */ function asset() external view override returns (address assetTokenAddress) { return address(underlying); } /** - * @notice The address of the underlying token used for the Vault uses for accounting, depositing, and withdrawing. - * Returns the total amount of the underlying asset that is “managed” by Vault. + * @return totalManagedAssets the total amount of the underlying asset tokens that is “managed” by Vault. */ function totalAssets() external view override returns (uint256 totalManagedAssets) { return underlying.balanceOf(address(this)); @@ -840,10 +839,11 @@ contract SavingsContract is ISavingsContractV4, Initializable, InitializableToke /** * @notice Mint exact amount of vault shares to the receiver by transferring enough underlying asset tokens from the caller. + * Emits a {Deposit} event. + * * @param shares The amount of vault shares to be minted. * @param receiver The account the vault shares will be minted to. * @return assets The amount of underlying assets that were transferred from the caller. - * Emits a {Deposit} event. */ function mint(uint256 shares, address receiver) external override returns (uint256 assets) { (assets, ) = _creditsToUnderlying(shares); @@ -869,26 +869,31 @@ contract SavingsContract is ISavingsContractV4, Initializable, InitializableToke } /** - * - * Returns Total number of underlying assets that caller can withdraw. + * @notice The maximum number of underlying assets that owner can withdraw. + * @param owner Address that owns the underlying assets. + * @return maxAssets The maximum amount of underlying assets the owner can withdraw. */ - function maxWithdraw(address caller) external view override returns (uint256 maxAssets) { - (maxAssets, ) = _creditsToUnderlying(balanceOf(caller)); + function maxWithdraw(address owner) external view override returns (uint256 maxAssets) { + (maxAssets, ) = _creditsToUnderlying(balanceOf(owner)); } /** * @notice Allows an on-chain or off-chain user to simulate the effects of their withdrawal at the current block, given current on-chain conditions. - * - * Return the exact amount of Vault shares that would be redeemed by the caller if withdrawing a given exact amount of underlying assets using the withdraw method. + * @param assets The amount of underlying assets to be withdrawn. + * @return shares The amount of vault shares that will be burnt. */ function previewWithdraw(uint256 assets) external view override returns (uint256 shares) { (shares, ) = _underlyingToCredits(assets); } /** - * Redeems shares from owner and sends assets of underlying tokens to receiver. - * Returns Total number of underlying shares redeemed. + * @notice Burns enough vault shares from owner and transfers the exact amount of underlying asset tokens to the receiver. * Emits a {Withdraw} event. + * + * @param assets The amount of underlying assets to be withdrawn from the vault. + * @param receiver The account that the underlying assets will be transferred to. + * @param owner Account that owns the vault shares to be burnt. + * @return shares The amount of vault shares that were burnt. */ function withdraw( uint256 assets, @@ -906,18 +911,19 @@ contract SavingsContract is ISavingsContractV4, Initializable, InitializableToke } /** - * @notice it must return a limited value if caller is subject to some withdrawal limit or timelock. must return balanceOf(caller) if caller is not subject to any withdrawal limit or timelock. MAY be used in the previewRedeem or redeem methods for shares input parameter. must NOT revert. + * @notice it must return a limited value if owner is subject to some withdrawal limit or timelock. must return balanceOf(owner) if owner is not subject to any withdrawal limit or timelock. MAY be used in the previewRedeem or redeem methods for shares input parameter. must NOT revert. * - * Returns Total number of underlying shares that caller can redeem. + * @param owner Address that owns the shares. + * @return maxShares Total number of shares that owner can redeem. */ - function maxRedeem(address caller) external view override returns (uint256 maxShares) { - maxShares = balanceOf(caller); + function maxRedeem(address owner) external view override returns (uint256 maxShares) { + maxShares = balanceOf(owner); } /** * @notice Allows an on-chain or off-chain user to simulate the effects of their redeemption at the current block, given current on-chain conditions. * - * Returns the exact amount of underlying assets that would be withdrawn by the caller if redeeming a given exact amount of Vault shares using the redeem method + * @return assets the exact amount of underlying assets that would be withdrawn by the caller if redeeming a given exact amount of Vault shares using the redeem method */ function previewRedeem(uint256 shares) external view override returns (uint256 assets) { (assets, ) = _creditsToUnderlying(shares); @@ -925,10 +931,13 @@ contract SavingsContract is ISavingsContractV4, Initializable, InitializableToke } /** - * Redeems shares from owner and sends assets of underlying tokens to receiver. - * - * Returns Total number of underlying assets of underlying redeemed. + * @notice Burns exact amount of vault shares from owner and transfers the underlying asset tokens to the receiver. * Emits a {Withdraw} event. + * + * @param shares The amount of vault shares to be burnt. + * @param receiver The account the underlying assets will be transferred to. + * @param owner The account that owns the vault shares to be burnt. + * @return assets The amount of underlying assets that were transferred to the receiver. */ function redeem( uint256 shares, From 3c7c4ff54f9e14532668a3d6be051f5b48f88f10 Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Thu, 21 Apr 2022 21:16:55 +1000 Subject: [PATCH 13/14] chore: restored older Solidity versions after rebase --- hardhat.config.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hardhat.config.ts b/hardhat.config.ts index d796b81c..5024ed32 100644 --- a/hardhat.config.ts +++ b/hardhat.config.ts @@ -59,7 +59,7 @@ export const hardhatConfig = { }, }, solidity: { - compilers: [{ ...compilerConfig("0.8.6") }], + compilers: [{ ...compilerConfig("0.8.6") }, { ...compilerConfig("0.8.2") }, { ...compilerConfig("0.5.16") }], }, paths: { artifacts: "./build" }, abiExporter: { From 38e3c29a211cd421b1ff0dd8d3d851f259fc31e2 Mon Sep 17 00:00:00 2001 From: doncesarts Date: Tue, 3 May 2022 13:50:13 +0100 Subject: [PATCH 14/14] chore: adds task deploySavingsContract4626 --- .../legacy-upgraded/imusd-mainnet-21.sol | 1859 ----------------- tasks-fork-polygon.config.ts | 1 + tasks-fork.config.ts | 1 + tasks.config.ts | 1 + tasks/deploySavingsContract4626.ts | 2 +- tasks/utils/emissions-split-buy-back.ts | 8 +- test/shared/ERC4626.behaviour.ts | 7 +- 7 files changed, 14 insertions(+), 1865 deletions(-) delete mode 100644 contracts/legacy-upgraded/imusd-mainnet-21.sol diff --git a/contracts/legacy-upgraded/imusd-mainnet-21.sol b/contracts/legacy-upgraded/imusd-mainnet-21.sol deleted file mode 100644 index 42b20155..00000000 --- a/contracts/legacy-upgraded/imusd-mainnet-21.sol +++ /dev/null @@ -1,1859 +0,0 @@ -pragma solidity 0.5.16; - -interface IUnwrapper { - // @dev Get bAssetOut status - function getIsBassetOut( - address _masset, - bool _inputIsCredit, - address _output - ) external view returns (bool isBassetOut); - - /// @dev Estimate output - function getUnwrapOutput( - bool _isBassetOut, - address _router, - address _input, - bool _inputIsCredit, - address _output, - uint256 _amount - ) external view returns (uint256 output); - - /// @dev Unwrap and send - function unwrapAndSend( - bool _isBassetOut, - address _router, - address _input, - address _output, - uint256 _amount, - uint256 _minAmountOut, - address _beneficiary - ) external returns (uint256 outputQuantity); -} - -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; -} - -interface ISavingsContractV1 { - function depositInterest(uint256 _amount) external; - - function depositSavings(uint256 _amount) external returns (uint256 creditsIssued); - - function redeem(uint256 _amount) external returns (uint256 massetReturned); - - function exchangeRate() external view returns (uint256); - - function creditBalances(address) external view returns (uint256); -} - -interface ISavingsContractV3 { - // 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 balance); // V2 - - function underlyingToCredits(uint256 _credits) external view returns (uint256 underlying); // V2 - - function creditsToUnderlying(uint256 _underlying) external view returns (uint256 credits); // V2 - - // -------------------------------------------- - - function redeemAndUnwrap( - uint256 _amount, - bool _isCreditAmt, - uint256 _minAmountOut, - address _output, - address _beneficiary, - address _router, - bool _isBassetOut - ) - external - returns ( - uint256 creditsBurned, - uint256 massetRedeemed, - uint256 outputQuantity - ); - - function depositSavings( - uint256 _underlying, - address _beneficiary, - address _referrer - ) external returns (uint256 creditsIssued); -} - -/* - * @dev Provides information about the current execution context, including the - * sender of the transaction and its data. While these are generally available - * via msg.sender and msg.data, they should not be accessed in such a direct - * manner, since when dealing with GSN meta-transactions the account sending and - * paying for execution may not be the actual sender (as far as an application - * is concerned). - * - * This contract is only required for intermediate, library-like contracts. - */ -contract Context { - // Empty internal constructor, to prevent people from mistakenly deploying - // an instance of this contract, which should be used via inheritance. - constructor() internal {} - - // solhint-disable-previous-line no-empty-blocks - - function _msgSender() internal view returns (address payable) { - return msg.sender; - } - - function _msgData() internal view returns (bytes memory) { - this; // silence state mutability warning without generating bytecode - see https://github.com/ethereum/solidity/issues/2691 - return msg.data; - } -} - -/** - * @dev Interface of the ERC20 standard as defined in the EIP. Does not include - * the optional functions; to access them see {ERC20Detailed}. - */ -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); -} - -/** - * @dev Wrappers over Solidity's arithmetic operations with added overflow - * checks. - * - * Arithmetic operations in Solidity wrap on overflow. This can easily result - * in bugs, because programmers usually assume that an overflow raises an - * error, which is the standard behavior in high level programming languages. - * `SafeMath` restores this intuition by reverting the transaction when an - * operation overflows. - * - * Using this library instead of the unchecked operations eliminates an entire - * class of bugs, so it's recommended to use it always. - */ -library SafeMath { - /** - * @dev Returns the addition of two unsigned integers, reverting on - * overflow. - * - * Counterpart to Solidity's `+` operator. - * - * Requirements: - * - Addition cannot overflow. - */ - function add(uint256 a, uint256 b) internal pure returns (uint256) { - uint256 c = a + b; - require(c >= a, "SafeMath: addition overflow"); - - return c; - } - - /** - * @dev Returns the subtraction of two unsigned integers, reverting on - * overflow (when the result is negative). - * - * Counterpart to Solidity's `-` operator. - * - * Requirements: - * - Subtraction cannot overflow. - */ - function sub(uint256 a, uint256 b) internal pure returns (uint256) { - return sub(a, b, "SafeMath: subtraction overflow"); - } - - /** - * @dev Returns the subtraction of two unsigned integers, reverting with custom message on - * overflow (when the result is negative). - * - * Counterpart to Solidity's `-` operator. - * - * Requirements: - * - Subtraction cannot overflow. - * - * _Available since v2.4.0._ - */ - function sub( - uint256 a, - uint256 b, - string memory errorMessage - ) internal pure returns (uint256) { - require(b <= a, errorMessage); - uint256 c = a - b; - - return c; - } - - /** - * @dev Returns the multiplication of two unsigned integers, reverting on - * overflow. - * - * Counterpart to Solidity's `*` operator. - * - * Requirements: - * - Multiplication cannot overflow. - */ - function mul(uint256 a, uint256 b) internal pure returns (uint256) { - // Gas optimization: this is cheaper than requiring 'a' not being zero, but the - // benefit is lost if 'b' is also tested. - // See: https://github.com/OpenZeppelin/openzeppelin-contracts/pull/522 - if (a == 0) { - return 0; - } - - uint256 c = a * b; - require(c / a == b, "SafeMath: multiplication overflow"); - - return c; - } - - /** - * @dev Returns the integer division of two unsigned integers. Reverts on - * division by zero. The result is rounded towards zero. - * - * Counterpart to Solidity's `/` operator. Note: this function uses a - * `revert` opcode (which leaves remaining gas untouched) while Solidity - * uses an invalid opcode to revert (consuming all remaining gas). - * - * Requirements: - * - The divisor cannot be zero. - */ - function div(uint256 a, uint256 b) internal pure returns (uint256) { - return div(a, b, "SafeMath: division by zero"); - } - - /** - * @dev Returns the integer division of two unsigned integers. Reverts with custom message on - * division by zero. The result is rounded towards zero. - * - * Counterpart to Solidity's `/` operator. Note: this function uses a - * `revert` opcode (which leaves remaining gas untouched) while Solidity - * uses an invalid opcode to revert (consuming all remaining gas). - * - * Requirements: - * - The divisor cannot be zero. - * - * _Available since v2.4.0._ - */ - function div( - uint256 a, - uint256 b, - string memory errorMessage - ) internal pure returns (uint256) { - // Solidity only automatically asserts when dividing by 0 - require(b > 0, errorMessage); - uint256 c = a / b; - // assert(a == b * c + a % b); // There is no case in which this doesn't hold - - return c; - } - - /** - * @dev Returns the remainder of dividing two unsigned integers. (unsigned integer modulo), - * Reverts when dividing by zero. - * - * Counterpart to Solidity's `%` operator. This function uses a `revert` - * opcode (which leaves remaining gas untouched) while Solidity uses an - * invalid opcode to revert (consuming all remaining gas). - * - * Requirements: - * - The divisor cannot be zero. - */ - function mod(uint256 a, uint256 b) internal pure returns (uint256) { - return mod(a, b, "SafeMath: modulo by zero"); - } - - /** - * @dev Returns the remainder of dividing two unsigned integers. (unsigned integer modulo), - * Reverts with custom message when dividing by zero. - * - * Counterpart to Solidity's `%` operator. This function uses a `revert` - * opcode (which leaves remaining gas untouched) while Solidity uses an - * invalid opcode to revert (consuming all remaining gas). - * - * Requirements: - * - The divisor cannot be zero. - * - * _Available since v2.4.0._ - */ - function mod( - uint256 a, - uint256 b, - string memory errorMessage - ) internal pure returns (uint256) { - require(b != 0, errorMessage); - return a % b; - } -} - -contract ERC20 is Context, IERC20 { - using SafeMath for uint256; - - mapping(address => uint256) private _balances; - - mapping(address => mapping(address => uint256)) private _allowances; - - uint256 private _totalSupply; - - /** - * @dev See {IERC20-totalSupply}. - */ - function totalSupply() public view returns (uint256) { - return _totalSupply; - } - - /** - * @dev See {IERC20-balanceOf}. - */ - function balanceOf(address account) public view returns (uint256) { - return _balances[account]; - } - - /** - * @dev See {IERC20-transfer}. - * - * Requirements: - * - * - `recipient` cannot be the zero address. - * - the caller must have a balance of at least `amount`. - */ - function transfer(address recipient, uint256 amount) public returns (bool) { - _transfer(_msgSender(), recipient, amount); - return true; - } - - /** - * @dev See {IERC20-allowance}. - */ - function allowance(address owner, address spender) public view returns (uint256) { - return _allowances[owner][spender]; - } - - /** - * @dev See {IERC20-approve}. - * - * Requirements: - * - * - `spender` cannot be the zero address. - */ - function approve(address spender, uint256 amount) public returns (bool) { - _approve(_msgSender(), spender, amount); - return true; - } - - /** - * @dev See {IERC20-transferFrom}. - * - * Emits an {Approval} event indicating the updated allowance. This is not - * required by the EIP. See the note at the beginning of {ERC20}; - * - * Requirements: - * - `sender` and `recipient` cannot be the zero address. - * - `sender` must have a balance of at least `amount`. - * - the caller must have allowance for `sender`'s tokens of at least - * `amount`. - */ - function transferFrom( - address sender, - address recipient, - uint256 amount - ) public returns (bool) { - _transfer(sender, recipient, amount); - _approve( - sender, - _msgSender(), - _allowances[sender][_msgSender()].sub( - amount, - "ERC20: transfer amount exceeds allowance" - ) - ); - return true; - } - - /** - * @dev Atomically increases the allowance granted to `spender` by the caller. - * - * This is an alternative to {approve} that can be used as a mitigation for - * problems described in {IERC20-approve}. - * - * Emits an {Approval} event indicating the updated allowance. - * - * Requirements: - * - * - `spender` cannot be the zero address. - */ - function increaseAllowance(address spender, uint256 addedValue) public returns (bool) { - _approve(_msgSender(), spender, _allowances[_msgSender()][spender].add(addedValue)); - return true; - } - - /** - * @dev Atomically decreases the allowance granted to `spender` by the caller. - * - * This is an alternative to {approve} that can be used as a mitigation for - * problems described in {IERC20-approve}. - * - * Emits an {Approval} event indicating the updated allowance. - * - * Requirements: - * - * - `spender` cannot be the zero address. - * - `spender` must have allowance for the caller of at least - * `subtractedValue`. - */ - function decreaseAllowance(address spender, uint256 subtractedValue) public returns (bool) { - _approve( - _msgSender(), - spender, - _allowances[_msgSender()][spender].sub( - subtractedValue, - "ERC20: decreased allowance below zero" - ) - ); - return true; - } - - /** - * @dev Moves tokens `amount` from `sender` to `recipient`. - * - * This is internal function is equivalent to {transfer}, and can be used to - * e.g. implement automatic token fees, slashing mechanisms, etc. - * - * Emits a {Transfer} event. - * - * Requirements: - * - * - `sender` cannot be the zero address. - * - `recipient` cannot be the zero address. - * - `sender` must have a balance of at least `amount`. - */ - function _transfer( - address sender, - address recipient, - uint256 amount - ) internal { - require(sender != address(0), "ERC20: transfer from the zero address"); - require(recipient != address(0), "ERC20: transfer to the zero address"); - - _balances[sender] = _balances[sender].sub(amount, "ERC20: transfer amount exceeds balance"); - _balances[recipient] = _balances[recipient].add(amount); - emit Transfer(sender, recipient, amount); - } - - /** @dev Creates `amount` tokens and assigns them to `account`, increasing - * the total supply. - * - * Emits a {Transfer} event with `from` set to the zero address. - * - * Requirements - * - * - `to` cannot be the zero address. - */ - function _mint(address account, uint256 amount) internal { - require(account != address(0), "ERC20: mint to the zero address"); - - _totalSupply = _totalSupply.add(amount); - _balances[account] = _balances[account].add(amount); - emit Transfer(address(0), account, amount); - } - - /** - * @dev Destroys `amount` tokens from `account`, reducing the - * total supply. - * - * Emits a {Transfer} event with `to` set to the zero address. - * - * Requirements - * - * - `account` cannot be the zero address. - * - `account` must have at least `amount` tokens. - */ - function _burn(address account, uint256 amount) internal { - require(account != address(0), "ERC20: burn from the zero address"); - - _balances[account] = _balances[account].sub(amount, "ERC20: burn amount exceeds balance"); - _totalSupply = _totalSupply.sub(amount); - emit Transfer(account, address(0), amount); - } - - /** - * @dev Sets `amount` as the allowance of `spender` over the `owner`s tokens. - * - * This is internal function is equivalent to `approve`, and can be used to - * e.g. set automatic allowances for certain subsystems, etc. - * - * Emits an {Approval} event. - * - * Requirements: - * - * - `owner` cannot be the zero address. - * - `spender` cannot be the zero address. - */ - function _approve( - address owner, - address spender, - uint256 amount - ) internal { - require(owner != address(0), "ERC20: approve from the zero address"); - require(spender != address(0), "ERC20: approve to the zero address"); - - _allowances[owner][spender] = amount; - emit Approval(owner, spender, amount); - } - - /** - * @dev Destroys `amount` tokens from `account`.`amount` is then deducted - * from the caller's allowance. - * - * See {_burn} and {_approve}. - */ - function _burnFrom(address account, uint256 amount) internal { - _burn(account, amount); - _approve( - account, - _msgSender(), - _allowances[account][_msgSender()].sub(amount, "ERC20: burn amount exceeds allowance") - ); - } -} - -contract InitializableERC20Detailed is IERC20 { - string private _name; - string private _symbol; - uint8 private _decimals; - - /** - * @dev Sets the values for `name`, `symbol`, and `decimals`. All three of - * these values are immutable: they can only be set once during - * construction. - * @notice To avoid variable shadowing appended `Arg` after arguments name. - */ - function _initialize( - string memory nameArg, - string memory symbolArg, - uint8 decimalsArg - ) internal { - _name = nameArg; - _symbol = symbolArg; - _decimals = decimalsArg; - } - - /** - * @dev Returns the name of the token. - */ - function name() public view returns (string memory) { - return _name; - } - - /** - * @dev Returns the symbol of the token, usually a shorter version of the - * name. - */ - function symbol() public view returns (string memory) { - return _symbol; - } - - /** - * @dev Returns the number of decimals used to get its user representation. - * For example, if `decimals` equals `2`, a balance of `505` tokens should - * be displayed to a user as `5,05` (`505 / 10 ** 2`). - * - * Tokens usually opt for a value of 18, imitating the relationship between - * Ether and Wei. - * - * NOTE: This information is only used for _display_ purposes: it in - * no way affects any of the arithmetic of the contract, including - * {IERC20-balanceOf} and {IERC20-transfer}. - */ - function decimals() public view returns (uint8) { - return _decimals; - } -} - -contract InitializableToken is ERC20, InitializableERC20Detailed { - /** - * @dev Initialization function for implementing contract - * @notice To avoid variable shadowing appended `Arg` after arguments name. - */ - function _initialize(string memory _nameArg, string memory _symbolArg) internal { - InitializableERC20Detailed._initialize(_nameArg, _symbolArg, 18); - } -} - -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; -} - -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; -} - -contract InitializableModule2 is ModuleKeys { - INexus public constant nexus = INexus(0xAFcE80b19A8cE13DEc0739a1aaB7A028d6845Eb3); - - /** - * @dev Modifier to allow function calls only from the Governor. - */ - modifier onlyGovernor() { - 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 Modifier to allow function calls only from the ProxyAdmin. - */ - modifier onlyProxyAdmin() { - require(msg.sender == _proxyAdmin(), "Only ProxyAdmin can execute"); - _; - } - - /** - * @dev Modifier to allow function calls only from the Manager. - */ - modifier onlyManager() { - require(msg.sender == _manager(), "Only manager 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 Staking Module address from the Nexus - * @return Address of the Staking Module contract - */ - function _staking() internal view returns (address) { - return nexus.getModule(KEY_STAKING); - } - - /** - * @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); - } - - /** - * @dev Return MetaToken Module address from the Nexus - * @return Address of the MetaToken Module contract - */ - function _metaToken() internal view returns (address) { - return nexus.getModule(KEY_META_TOKEN); - } - - /** - * @dev Return OracleHub Module address from the Nexus - * @return Address of the OracleHub Module contract - */ - function _oracleHub() internal view returns (address) { - return nexus.getModule(KEY_ORACLE_HUB); - } - - /** - * @dev Return Manager Module address from the Nexus - * @return Address of the Manager Module contract - */ - function _manager() internal view returns (address) { - return nexus.getModule(KEY_MANAGER); - } - - /** - * @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); - } -} - -interface IConnector { - /** - * @notice Deposits the mAsset into the connector - * @param _amount Units of mAsset to receive and deposit - */ - function deposit(uint256 _amount) external; - - /** - * @notice Withdraws a specific amount of mAsset from the connector - * @param _amount Units of mAsset to withdraw - */ - function withdraw(uint256 _amount) external; - - /** - * @notice Withdraws all mAsset from the connector - */ - function withdrawAll() external; - - /** - * @notice Returns the available balance in the connector. In connections - * where there is likely to be an initial dip in value due to conservative - * exchange rates (e.g. with Curves `get_virtual_price`), it should return - * max(deposited, balance) to avoid temporary negative yield. Any negative yield - * should be corrected during a withdrawal or over time. - * @return Balance of mAsset in the connector - */ - function checkBalance() external view returns (uint256); -} - -contract Initializable { - /** - * @dev Indicates that the contract has been initialized. - */ - bool private initialized; - - /** - * @dev Indicates that the contract is in the process of being initialized. - */ - bool private initializing; - - /** - * @dev Modifier to use in the initializer function of a contract. - */ - modifier initializer() { - require( - initializing || isConstructor() || !initialized, - "Contract instance has already been initialized" - ); - - bool isTopLevelCall = !initializing; - if (isTopLevelCall) { - initializing = true; - initialized = true; - } - - _; - - if (isTopLevelCall) { - initializing = false; - } - } - - /// @dev Returns true if and only if the function is running in the constructor - function isConstructor() private view returns (bool) { - // extcodesize checks the size of the code stored in an address, and - // address returns the current address. Since the code is still not - // deployed when running a constructor, any checks on its code size will - // yield zero, making it an effective way to detect if a contract is - // under construction or not. - address self = address(this); - uint256 cs; - assembly { - cs := extcodesize(self) - } - return cs == 0; - } - - // Reserved storage space to allow for layout changes in the future. - uint256[50] private ______gap; -} - -library StableMath { - using SafeMath for uint256; - - /** - * @dev Scaling unit for use in specific calculations, - * where 1 * 10**18, or 1e18 represents a unit '1' - */ - uint256 private constant FULL_SCALE = 1e18; - - /** - * @notice 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) - * @dev 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.mul(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 - uint256 z = x.mul(y); - // return 9e38 / 1e18 = 9e18 - return z.div(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.mul(y); - // e.g. 138145381104e17 + 9.99...e17 = 138145381113.99...e17 - uint256 ceil = scaled.add(FULL_SCALE.sub(1)); - // e.g. 13814538111.399...e18 / 1e18 = 13814538111 - return ceil.div(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 - uint256 z = x.mul(FULL_SCALE); - // e.g. 8e36 / 10e18 = 8e17 - return z.div(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 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.mul(ratio); - // 1e26 + 9.99e7 = 100..00.999e8 - uint256 ceil = scaled.add(RATIO_SCALE.sub(1)); - // return 100..00.999e8 / 1e8 = 1e18 - return ceil.div(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 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 - uint256 y = x.mul(RATIO_SCALE); - // return 1e22 / 1e12 = 1e10 - return y.div(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; - } -} - -/** - * @title SavingsContract - * @author Stability Labs Pty. Ltd. - * @notice Savings contract uses the ever increasing "exchangeRate" to increase - * the value of the Savers "credits" (ERC20) relative to the amount of additional - * underlying collateral that has been deposited into this contract ("interest") - * @dev VERSION: 2.1 - * DATE: 2021-11-25 - */ -contract SavingsContract_imusd_mainnet_21 is - ISavingsContractV1, - ISavingsContractV3, - Initializable, - InitializableToken, - InitializableModule2 -{ - using SafeMath for uint256; - using StableMath for uint256; - - // Core events for depositing and withdrawing - event ExchangeRateUpdated(uint256 newExchangeRate, uint256 interestCollected); - event SavingsDeposited(address indexed saver, uint256 savingsDeposited, uint256 creditsIssued); - event CreditsRedeemed( - address indexed redeemer, - uint256 creditsRedeemed, - uint256 savingsCredited - ); - - event AutomaticInterestCollectionSwitched(bool automationEnabled); - - // Connector poking - event PokerUpdated(address poker); - - event FractionUpdated(uint256 fraction); - event ConnectorUpdated(address connector); - event EmergencyUpdate(); - - event Poked(uint256 oldBalance, uint256 newBalance, uint256 interestDetected); - event PokedRaw(); - - // Tracking events - event Referral(address indexed referrer, address beneficiary, uint256 amount); - - // Rate between 'savings credits' and underlying - // e.g. 1 credit (1e17) mulTruncate(exchangeRate) = underlying, starts at 10:1 - // exchangeRate increases over time - uint256 private constant startingRate = 1e17; - uint256 public exchangeRate; - - // Underlying asset is underlying - IERC20 public constant underlying = IERC20(0xe2f2a5C287993345a840Db3B0845fbC70f5935a5); - bool private automateInterestCollection; - - // Yield - // Poker is responsible for depositing/withdrawing from connector - address public poker; - // Last time a poke was made - uint256 public lastPoke; - // Last known balance of the connector - uint256 public lastBalance; - // Fraction of capital assigned to the connector (100% = 1e18) - uint256 public fraction; - // Address of the current connector (all IConnectors are mStable validated) - IConnector public connector; - // How often do we allow pokes - uint256 private constant POKE_CADENCE = 4 hours; - // Max APY generated on the capital in the connector - uint256 private constant MAX_APY = 4e18; - uint256 private constant SECONDS_IN_YEAR = 365 days; - // Proxy contract for easy redemption - address public constant unwrapper = 0xc1443Cb9ce81915fB914C270d74B0D57D1c87be0; - - // Add these constants to bytecode at deploytime - function initialize( - address _poker, - string calldata _nameArg, - string calldata _symbolArg - ) external initializer { - InitializableToken._initialize(_nameArg, _symbolArg); - - require(_poker != address(0), "Invalid poker address"); - poker = _poker; - - fraction = 2e17; - automateInterestCollection = true; - exchangeRate = startingRate; - } - - /** @dev Only the savings managaer (pulled from Nexus) can execute this */ - modifier onlySavingsManager() { - require(msg.sender == _savingsManager(), "Only savings manager can execute"); - _; - } - - /*************************************** - VIEW - E - ****************************************/ - - /** - * @dev Returns the underlying balance of a given user - * @param _user Address of the user to check - * @return balance Units of underlying owned by the user - */ - function balanceOfUnderlying(address _user) external view returns (uint256 balance) { - (balance, ) = _creditsToUnderlying(balanceOf(_user)); - } - - /** - * @dev Converts a given underlying amount into credits - * @param _underlying Units of underlying - * @return credits Credit units (a.k.a imUSD) - */ - function underlyingToCredits(uint256 _underlying) external view returns (uint256 credits) { - (credits, ) = _underlyingToCredits(_underlying); - } - - /** - * @dev Converts a given credit amount into underlying - * @param _credits Units of credits - * @return amount Corresponding underlying amount - */ - function creditsToUnderlying(uint256 _credits) external view returns (uint256 amount) { - (amount, ) = _creditsToUnderlying(_credits); - } - - // Deprecated in favour of `balanceOf(address)` - // Maintained for backwards compatibility - // Returns the credit balance of a given user - function creditBalances(address _user) external view returns (uint256) { - return balanceOf(_user); - } - - /*************************************** - INTEREST - ****************************************/ - - /** - * @dev Deposit interest (add to savings) and update exchange rate of contract. - * Exchange rate is calculated as the ratio between new savings q and credits: - * exchange rate = savings / credits - * - * @param _amount Units of underlying to add to the savings vault - */ - function depositInterest(uint256 _amount) external onlySavingsManager { - require(_amount > 0, "Must deposit something"); - - // Transfer the interest from sender to here - require(underlying.transferFrom(msg.sender, address(this), _amount), "Must receive tokens"); - - // Calc new exchange rate, protect against initialisation case - uint256 totalCredits = totalSupply(); - if (totalCredits > 0) { - // new exchange rate is relationship between _totalCredits & totalSavings - // _totalCredits * exchangeRate = totalSavings - // exchangeRate = totalSavings/_totalCredits - (uint256 totalCollat, ) = _creditsToUnderlying(totalCredits); - uint256 newExchangeRate = _calcExchangeRate(totalCollat.add(_amount), totalCredits); - exchangeRate = newExchangeRate; - - emit ExchangeRateUpdated(newExchangeRate, _amount); - } - } - - /** @dev Enable or disable the automation of fee collection during deposit process */ - function automateInterestCollectionFlag(bool _enabled) external onlyGovernor { - automateInterestCollection = _enabled; - emit AutomaticInterestCollectionSwitched(_enabled); - } - - /*************************************** - DEPOSIT - ****************************************/ - - /** - * @dev During a migration period, allow savers to deposit underlying here before the interest has been redirected - * @param _underlying Units of underlying to deposit into savings vault - * @param _beneficiary Immediately transfer the imUSD token to this beneficiary address - * @return creditsIssued Units of credits (imUSD) issued - */ - function preDeposit(uint256 _underlying, address _beneficiary) - external - returns (uint256 creditsIssued) - { - require(exchangeRate == startingRate, "Can only use this method before streaming begins"); - return _deposit(_underlying, _beneficiary, false); - } - - /** - * @dev Deposit the senders savings to the vault, and credit them internally with "credits". - * Credit amount is calculated as a ratio of deposit amount and exchange rate: - * credits = underlying / exchangeRate - * We will first update the internal exchange rate by collecting any interest generated on the underlying. - * @param _underlying Units of underlying to deposit into savings vault - * @return creditsIssued Units of credits (imUSD) issued - */ - function depositSavings(uint256 _underlying) external returns (uint256 creditsIssued) { - return _deposit(_underlying, msg.sender, true); - } - - /** - * @dev Deposit the senders savings to the vault, and credit them internally with "credits". - * Credit amount is calculated as a ratio of deposit amount and exchange rate: - * credits = underlying / exchangeRate - * We will first update the internal exchange rate by collecting any interest generated on the underlying. - * @param _underlying Units of underlying to deposit into savings vault - * @param _beneficiary Immediately transfer the imUSD token to this beneficiary address - * @return creditsIssued Units of credits (imUSD) issued - */ - function depositSavings(uint256 _underlying, address _beneficiary) - external - returns (uint256 creditsIssued) - { - return _deposit(_underlying, _beneficiary, true); - } - - /** - * @dev Overloaded `depositSavings` method with an optional referrer address. - * @param _underlying Units of underlying to deposit into savings vault - * @param _beneficiary Immediately transfer the imUSD token to this beneficiary address - * @param _referrer Referrer address for this deposit - * @return creditsIssued Units of credits (imUSD) issued - */ - function depositSavings( - uint256 _underlying, - address _beneficiary, - address _referrer - ) external returns (uint256 creditsIssued) { - emit Referral(_referrer, _beneficiary, _underlying); - return _deposit(_underlying, _beneficiary, true); - } - - /** - * @dev Internally deposit the _underlying from the sender and credit the beneficiary with new imUSD - */ - function _deposit( - uint256 _underlying, - address _beneficiary, - bool _collectInterest - ) internal returns (uint256 creditsIssued) { - require(_underlying > 0, "Must deposit something"); - require(_beneficiary != address(0), "Invalid beneficiary address"); - - // Collect recent interest generated by basket and update exchange rate - IERC20 mAsset = underlying; - if (_collectInterest) { - ISavingsManager(_savingsManager()).collectAndDistributeInterest(address(mAsset)); - } - - // Transfer tokens from sender to here - require(mAsset.transferFrom(msg.sender, address(this), _underlying), "Must receive tokens"); - - // Calc how many credits they receive based on currentRatio - (creditsIssued, ) = _underlyingToCredits(_underlying); - - // add credits to ERC20 balances - _mint(_beneficiary, creditsIssued); - - emit SavingsDeposited(_beneficiary, _underlying, creditsIssued); - } - - /*************************************** - REDEEM - ****************************************/ - - // Deprecated in favour of redeemCredits - // Maintaining backwards compatibility, this fn minimics the old redeem fn, in which - // credits are redeemed but the interest from the underlying is not collected. - function redeem(uint256 _credits) external returns (uint256 massetReturned) { - require(_credits > 0, "Must withdraw something"); - - (, uint256 payout) = _redeem(_credits, true, true); - - // Collect recent interest generated by basket and update exchange rate - if (automateInterestCollection) { - ISavingsManager(_savingsManager()).collectAndDistributeInterest(address(underlying)); - } - - return payout; - } - - /** - * @dev Redeem specific number of the senders "credits" in exchange for underlying. - * Payout amount is calculated as a ratio of credits and exchange rate: - * payout = credits * exchangeRate - * @param _credits Amount of credits to redeem - * @return massetReturned Units of underlying mAsset paid out - */ - function redeemCredits(uint256 _credits) external returns (uint256 massetReturned) { - require(_credits > 0, "Must withdraw something"); - - // Collect recent interest generated by basket and update exchange rate - if (automateInterestCollection) { - ISavingsManager(_savingsManager()).collectAndDistributeInterest(address(underlying)); - } - - (, uint256 payout) = _redeem(_credits, true, true); - - return payout; - } - - /** - * @dev Redeem credits into a specific amount of underlying. - * Credits needed to burn is calculated using: - * credits = underlying / exchangeRate - * @param _underlying Amount of underlying to redeem - * @return creditsBurned Units of credits burned from sender - */ - function redeemUnderlying(uint256 _underlying) external returns (uint256 creditsBurned) { - require(_underlying > 0, "Must withdraw something"); - - // Collect recent interest generated by basket and update exchange rate - if (automateInterestCollection) { - ISavingsManager(_savingsManager()).collectAndDistributeInterest(address(underlying)); - } - - // Ensure that the payout was sufficient - (uint256 credits, uint256 massetReturned) = _redeem(_underlying, false, true); - require(massetReturned == _underlying, "Invalid output"); - - return credits; - } - - /** - * @notice Redeem credits into a specific amount of underlying, unwrap - * into a selected output asset, and send to a beneficiary - * Credits needed to burn is calculated using: - * credits = underlying / exchangeRate - * @param _amount Units to redeem (either underlying or credit amount). - * @param _isCreditAmt `true` if `amount` is in credits. eg imUSD. `false` if `amount` is in underlying. eg mUSD. - * @param _minAmountOut Minimum amount of `output` tokens to unwrap for. This is to the same decimal places as the `output` token. - * @param _output Asset to receive in exchange for the redeemed mAssets. This can be a bAsset or a fAsset. For example: - - bAssets (USDC, DAI, sUSD or USDT) or fAssets (GUSD, BUSD, alUSD, FEI or RAI) for mainnet imUSD Vault. - - bAssets (USDC, DAI or USDT) or fAsset FRAX for Polygon imUSD Vault. - - bAssets (WBTC, sBTC or renBTC) or fAssets (HBTC or TBTCV2) for mainnet imBTC Vault. - * @param _beneficiary Address to send `output` tokens to. - * @param _router mAsset address if the output is a bAsset. Feeder Pool address if the output is a fAsset. - * @param _isBassetOut `true` if `output` is a bAsset. `false` if `output` is a fAsset. - * @return creditsBurned Units of credits burned from sender. eg imUSD or imBTC. - * @return massetReturned Units of the underlying mAssets that were redeemed or swapped for the output tokens. eg mUSD or mBTC. - * @return outputQuantity Units of `output` tokens sent to the beneficiary. - */ - function redeemAndUnwrap( - uint256 _amount, - bool _isCreditAmt, - uint256 _minAmountOut, - address _output, - address _beneficiary, - address _router, - bool _isBassetOut - ) - external - returns ( - uint256 creditsBurned, - uint256 massetReturned, - uint256 outputQuantity - ) - { - require(_amount > 0, "Must withdraw something"); - require(_output != address(0), "Output address is zero"); - require(_beneficiary != address(0), "Beneficiary address is zero"); - require(_router != address(0), "Router address is zero"); - - // Collect recent interest generated by basket and update exchange rate - if (automateInterestCollection) { - ISavingsManager(_savingsManager()).collectAndDistributeInterest(address(underlying)); - } - - // Ensure that the payout was sufficient - (creditsBurned, massetReturned) = _redeem(_amount, _isCreditAmt, false); - require( - _isCreditAmt ? creditsBurned == _amount : massetReturned == _amount, - "Invalid output" - ); - - // Approve wrapper to spend contract's underlying; just for this tx - underlying.approve(unwrapper, massetReturned); - - // Unwrap the underlying into `output` and transfer to `beneficiary` - outputQuantity = IUnwrapper(unwrapper).unwrapAndSend( - _isBassetOut, - _router, - address(underlying), - _output, - massetReturned, - _minAmountOut, - _beneficiary - ); - } - - /** - * @dev Internally burn the credits and send the underlying to msg.sender - */ - function _redeem( - uint256 _amt, - bool _isCreditAmt, - bool _transferUnderlying - ) internal returns (uint256 creditsBurned, uint256 massetReturned) { - // Centralise credit <> underlying calcs and minimise SLOAD count - uint256 credits_; - uint256 underlying_; - uint256 exchangeRate_; - // If the input is a credit amt, then calculate underlying payout and cache the exchangeRate - if (_isCreditAmt) { - credits_ = _amt; - (underlying_, exchangeRate_) = _creditsToUnderlying(_amt); - } - // If the input is in underlying, then calculate credits needed to burn - else { - underlying_ = _amt; - (credits_, exchangeRate_) = _underlyingToCredits(_amt); - } - - // Burn required credits from the sender FIRST - _burn(msg.sender, credits_); - // Optionally, transfer tokens from here to sender - if (_transferUnderlying) { - require(underlying.transfer(msg.sender, underlying_), "Must send tokens"); - } - - // If this withdrawal pushes the portion of stored collateral in the `connector` over a certain - // threshold (fraction + 20%), then this should trigger a _poke on the connector. This is to avoid - // a situation in which there is a rush on withdrawals for some reason, causing the connector - // balance to go up and thus having too large an exposure. - CachedData memory cachedData = _cacheData(); - ConnectorStatus memory status = _getConnectorStatus(cachedData, exchangeRate_); - if (status.inConnector > status.limit) { - _poke(cachedData, false); - } - - emit CreditsRedeemed(msg.sender, credits_, underlying_); - - return (credits_, underlying_); - } - - struct ConnectorStatus { - // Limit is the max amount of units allowed in the connector - uint256 limit; - // Derived balance of the connector - uint256 inConnector; - } - - /** - * @dev Derives the units of collateral held in the connector - * @param _data Struct containing data on balances - * @param _exchangeRate Current system exchange rate - * @return status Contains max amount of assets allowed in connector - */ - function _getConnectorStatus(CachedData memory _data, uint256 _exchangeRate) - internal - pure - returns (ConnectorStatus memory) - { - // Total units of underlying collateralised - uint256 totalCollat = _data.totalCredits.mulTruncate(_exchangeRate); - // Max amount of underlying that can be held in the connector - uint256 limit = totalCollat.mulTruncate(_data.fraction.add(2e17)); - // Derives amount of underlying present in the connector - uint256 inConnector = _data.rawBalance >= totalCollat - ? 0 - : totalCollat.sub(_data.rawBalance); - - return ConnectorStatus(limit, inConnector); - } - - /*************************************** - YIELD - E - ****************************************/ - - /** @dev Modifier allowing only the designated poker to execute the fn */ - modifier onlyPoker() { - require(msg.sender == poker, "Only poker can execute"); - _; - } - - /** - * @dev External poke function allows for the redistribution of collateral between here and the - * current connector, setting the ratio back to the defined optimal. - */ - function poke() external onlyPoker { - CachedData memory cachedData = _cacheData(); - _poke(cachedData, false); - } - - /** - * @dev Governance action to set the address of a new poker - * @param _newPoker Address of the new poker - */ - function setPoker(address _newPoker) external onlyGovernor { - require(_newPoker != address(0) && _newPoker != poker, "Invalid poker"); - - poker = _newPoker; - - emit PokerUpdated(_newPoker); - } - - /** - * @dev Governance action to set the percentage of assets that should be held - * in the connector. - * @param _fraction Percentage of assets that should be held there (where 20% == 2e17) - */ - function setFraction(uint256 _fraction) external onlyGovernor { - require(_fraction <= 5e17, "Fraction must be <= 50%"); - - fraction = _fraction; - - CachedData memory cachedData = _cacheData(); - _poke(cachedData, true); - - emit FractionUpdated(_fraction); - } - - /** - * @dev Governance action to set the address of a new connector, and move funds (if any) across. - * @param _newConnector Address of the new connector - */ - function setConnector(address _newConnector) external onlyGovernor { - // Withdraw all from previous by setting target = 0 - CachedData memory cachedData = _cacheData(); - cachedData.fraction = 0; - _poke(cachedData, true); - - // Set new connector - CachedData memory cachedDataNew = _cacheData(); - connector = IConnector(_newConnector); - _poke(cachedDataNew, true); - - emit ConnectorUpdated(_newConnector); - } - - /** - * @dev Governance action to perform an emergency withdraw of the assets in the connector, - * should it be the case that some or all of the liquidity is trapped in. This causes the total - * collateral in the system to go down, causing a hard refresh. - */ - function emergencyWithdraw(uint256 _withdrawAmount) external onlyGovernor { - // withdraw _withdrawAmount from connection - connector.withdraw(_withdrawAmount); - - // reset the connector - connector = IConnector(address(0)); - emit ConnectorUpdated(address(0)); - - // set fraction to 0 - fraction = 0; - emit FractionUpdated(0); - - // check total collateralisation of credits - CachedData memory data = _cacheData(); - // use rawBalance as the remaining liquidity in the connector is now written off - _refreshExchangeRate(data.rawBalance, data.totalCredits, true); - - emit EmergencyUpdate(); - } - - /*************************************** - YIELD - I - ****************************************/ - - /** @dev Internal poke function to keep the balance between connector and raw balance healthy */ - function _poke(CachedData memory _data, bool _ignoreCadence) internal { - require(_data.totalCredits > 0, "Must have something to poke"); - - // 1. Verify that poke cadence is valid, unless this is a manual action by governance - uint256 currentTime = uint256(now); - uint256 timeSinceLastPoke = currentTime.sub(lastPoke); - require(_ignoreCadence || timeSinceLastPoke > POKE_CADENCE, "Not enough time elapsed"); - lastPoke = currentTime; - - // If there is a connector, check the balance and settle to the specified fraction % - IConnector connector_ = connector; - if (address(connector_) != address(0)) { - // 2. Check and verify new connector balance - uint256 lastBalance_ = lastBalance; - uint256 connectorBalance = connector_.checkBalance(); - // Always expect the collateral in the connector to increase in value - require(connectorBalance >= lastBalance_, "Invalid yield"); - if (connectorBalance > 0) { - // Validate the collection by ensuring that the APY is not ridiculous - _validateCollection( - connectorBalance, - connectorBalance.sub(lastBalance_), - timeSinceLastPoke - ); - } - - // 3. Level the assets to Fraction (connector) & 100-fraction (raw) - uint256 sum = _data.rawBalance.add(connectorBalance); - uint256 ideal = sum.mulTruncate(_data.fraction); - // If there is not enough mAsset in the connector, then deposit - if (ideal > connectorBalance) { - uint256 deposit = ideal.sub(connectorBalance); - underlying.approve(address(connector_), deposit); - connector_.deposit(deposit); - } - // Else withdraw, if there is too much mAsset in the connector - else if (connectorBalance > ideal) { - // If fraction == 0, then withdraw everything - if (ideal == 0) { - connector_.withdrawAll(); - sum = IERC20(underlying).balanceOf(address(this)); - } else { - connector_.withdraw(connectorBalance.sub(ideal)); - } - } - // Else ideal == connectorBalance (e.g. 0), do nothing - require(connector_.checkBalance() >= ideal, "Enforce system invariant"); - - // 4i. Refresh exchange rate and emit event - lastBalance = ideal; - _refreshExchangeRate(sum, _data.totalCredits, false); - emit Poked(lastBalance_, ideal, connectorBalance.sub(lastBalance_)); - } else { - // 4ii. Refresh exchange rate and emit event - lastBalance = 0; - _refreshExchangeRate(_data.rawBalance, _data.totalCredits, false); - emit PokedRaw(); - } - } - - /** - * @dev Internal fn to refresh the exchange rate, based on the sum of collateral and the number of credits - * @param _realSum Sum of collateral held by the contract - * @param _totalCredits Total number of credits in the system - * @param _ignoreValidation This is for use in the emergency situation, and ignores a decreasing exchangeRate - */ - function _refreshExchangeRate( - uint256 _realSum, - uint256 _totalCredits, - bool _ignoreValidation - ) internal { - // Based on the current exchange rate, how much underlying is collateralised? - (uint256 totalCredited, ) = _creditsToUnderlying(_totalCredits); - - // Require the amount of capital held to be greater than the previously credited units - require(_ignoreValidation || _realSum >= totalCredited, "ExchangeRate must increase"); - // Work out the new exchange rate based on the current capital - uint256 newExchangeRate = _calcExchangeRate(_realSum, _totalCredits); - exchangeRate = newExchangeRate; - - emit ExchangeRateUpdated( - newExchangeRate, - _realSum > totalCredited ? _realSum.sub(totalCredited) : 0 - ); - } - - /** - * FORKED DIRECTLY FROM SAVINGSMANAGER.sol - * --------------------------------------- - * @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 _newBalance New balance of the underlying - * @param _interest Increase in total supply since last collection - * @param _timeSinceLastCollection Seconds since last collection - */ - function _validateCollection( - uint256 _newBalance, - uint256 _interest, - uint256 _timeSinceLastCollection - ) internal pure returns (uint256 extrapolatedAPY) { - // Protect against division by 0 - uint256 protectedTime = StableMath.max(1, _timeSinceLastCollection); - - uint256 oldSupply = _newBalance.sub(_interest); - uint256 percentageIncrease = _interest.divPrecisely(oldSupply); - - uint256 yearsSinceLastCollection = protectedTime.divPrecisely(SECONDS_IN_YEAR); - - extrapolatedAPY = percentageIncrease.divPrecisely(yearsSinceLastCollection); - - if (protectedTime > 30 minutes) { - require(extrapolatedAPY < MAX_APY, "Interest protected from inflating past maxAPY"); - } else { - require(percentageIncrease < 1e15, "Interest protected from inflating past 10 Bps"); - } - } - - /*************************************** - VIEW - I - ****************************************/ - - struct CachedData { - // SLOAD from 'fraction' - uint256 fraction; - // ERC20 balance of underlying, held by this contract - // underlying.balanceOf(address(this)) - uint256 rawBalance; - // totalSupply() - uint256 totalCredits; - } - - /** - * @dev Retrieves generic data to avoid duplicate SLOADs - */ - function _cacheData() internal view returns (CachedData memory) { - uint256 balance = underlying.balanceOf(address(this)); - return CachedData(fraction, balance, totalSupply()); - } - - /** - * @dev Converts masset amount into credits based on exchange rate - * c = (masset / exchangeRate) + 1 - */ - function _underlyingToCredits(uint256 _underlying) - internal - view - returns (uint256 credits, uint256 exchangeRate_) - { - // e.g. (1e20 * 1e18) / 1e18 = 1e20 - // e.g. (1e20 * 1e18) / 14e17 = 7.1429e19 - // e.g. 1 * 1e18 / 1e17 + 1 = 11 => 11 * 1e17 / 1e18 = 1.1e18 / 1e18 = 1 - exchangeRate_ = exchangeRate; - credits = _underlying.divPrecisely(exchangeRate_).add(1); - } - - /** - * @dev Works out a new exchange rate, given an amount of collateral and total credits - * e = underlying / (credits-1) - */ - function _calcExchangeRate(uint256 _totalCollateral, uint256 _totalCredits) - internal - pure - returns (uint256 _exchangeRate) - { - _exchangeRate = _totalCollateral.divPrecisely(_totalCredits.sub(1)); - } - - /** - * @dev Converts credit amount into masset based on exchange rate - * m = credits * exchangeRate - */ - function _creditsToUnderlying(uint256 _credits) - internal - view - returns (uint256 underlyingAmount, uint256 exchangeRate_) - { - // e.g. (1e20 * 1e18) / 1e18 = 1e20 - // e.g. (1e20 * 14e17) / 1e18 = 1.4e20 - exchangeRate_ = exchangeRate; - underlyingAmount = _credits.mulTruncate(exchangeRate_); - } -} diff --git a/tasks-fork-polygon.config.ts b/tasks-fork-polygon.config.ts index f6d2120b..83c6a5ce 100644 --- a/tasks-fork-polygon.config.ts +++ b/tasks-fork-polygon.config.ts @@ -4,6 +4,7 @@ import "./tasks/deployEmissionsController" import "./tasks/deployIntegration" import "./tasks/deployFeeders" import "./tasks/deployPolygon" +import "./tasks/deploySavingsContract4626" import "./tasks/deployUnwrapper" import "./tasks/bridge" import "./tasks/emissions" diff --git a/tasks-fork.config.ts b/tasks-fork.config.ts index e2b8cb30..73bc02c7 100644 --- a/tasks-fork.config.ts +++ b/tasks-fork.config.ts @@ -10,6 +10,7 @@ import "./tasks/deployPolygon" import "./tasks/deployRevenueForwarder" import "./tasks/deployBriber" import "./tasks/deploySavingsManager" +import "./tasks/deploySavingsContract4626" import "./tasks/deployUnwrapper" import "./tasks/bridge" import "./tasks/dials" diff --git a/tasks.config.ts b/tasks.config.ts index ee1eb60a..af5504fc 100644 --- a/tasks.config.ts +++ b/tasks.config.ts @@ -10,6 +10,7 @@ import "./tasks/deployPolygon" import "./tasks/deployRevenueForwarder" import "./tasks/deployBriber" import "./tasks/deploySavingsManager" +import "./tasks/deploySavingsContract4626" import "./tasks/deployUnwrapper" import "./tasks/bridge" import "./tasks/dials" diff --git a/tasks/deploySavingsContract4626.ts b/tasks/deploySavingsContract4626.ts index 63ee9d5c..51731b1f 100644 --- a/tasks/deploySavingsContract4626.ts +++ b/tasks/deploySavingsContract4626.ts @@ -34,7 +34,7 @@ task("upgrade-imusd-polygon", "Upgrade Polygon imUSD save contract imUSD") const unwrapperAddress = getChainAddress("Unwrapper", chain) const constructorArguments = [nexusAddress, musdAddress, unwrapperAddress] - // Deploy step 1 - Save Vault + // Deploy step 1 - Save Contract const saveContractImpl = await deployContract( new SavingsContractImusdPolygon22__factory(signer), "mStable: mUSD Savings Contract (imUSD)", diff --git a/tasks/utils/emissions-split-buy-back.ts b/tasks/utils/emissions-split-buy-back.ts index 2f6f9d19..82b395ad 100644 --- a/tasks/utils/emissions-split-buy-back.ts +++ b/tasks/utils/emissions-split-buy-back.ts @@ -2,7 +2,7 @@ import { Signer } from "@ethersproject/abstract-signer" import { ContractTransaction } from "ethers" import { BN, simpleToExactAmount } from "@utils/math" -import { RevenueSplitBuyBack, ERC20__factory } from "types/generated" +import { RevenueSplitBuyBack, ERC20__factory, IERC20Metadata } from "types/generated" import { EncodedPaths, encodeUniswapPath, getWETHPath, quoteSwap } from "@utils/peripheral/uniswap" export interface MAssetSwap { @@ -66,15 +66,15 @@ export const calculateBuyBackRewardsQuote = async (signer: Signer, params: MainP const treasuryFee: BN = await revenueSplitBuyBack.treasuryFee() const rewardsToken = await revenueSplitBuyBack.REWARDS_TOKEN() - const rewardsTokenContract = ERC20__factory.connect(rewardsToken, signer) + const rewardsTokenContract = (ERC20__factory.connect(rewardsToken, signer) as unknown as IERC20Metadata) const rTokenDecimals = await rewardsTokenContract.decimals() const rTokenSymbol = await rewardsTokenContract.symbol() for (let i = 0; i < mAssets.length; i = 1 + 1) { const mAsset = mAssets[i] const bAsset: string = await revenueSplitBuyBack.bassets(mAsset.address) - const mAssetContract = ERC20__factory.connect(mAsset.address, signer) - const bAssetContract = ERC20__factory.connect(bAsset, signer) + const mAssetContract = (ERC20__factory.connect(mAsset.address, signer)as unknown as IERC20Metadata) + const bAssetContract = (ERC20__factory.connect(bAsset, signer)as unknown as IERC20Metadata) const mAssetBalance: BN = await mAssetContract.balanceOf(revenueSplitBuyBack.address) const mAssetSymbol: string = await mAssetContract.symbol() diff --git a/test/shared/ERC4626.behaviour.ts b/test/shared/ERC4626.behaviour.ts index 8cede53a..0bbc1a6d 100644 --- a/test/shared/ERC4626.behaviour.ts +++ b/test/shared/ERC4626.behaviour.ts @@ -1,4 +1,4 @@ -import { ZERO_ADDRESS } from "@utils/constants" +import { ZERO, ZERO_ADDRESS } from "@utils/constants" import { MassetDetails, MassetMachine, StandardAccounts } from "@utils/machines" import { BN, simpleToExactAmount, safeInfinity } from "@utils/math" import { expect } from "chai" @@ -58,6 +58,11 @@ export function shouldBehaveLikeERC4626(ctx: IERC4626BehaviourContext): void { "Invalid beneficiary address", ) }) + it("fails if preview amount is zero", async () => { + await expect(ctx.vault.connect(ctx.sa.default.signer).previewDeposit(ZERO)).to.be.revertedWith( + "Must deposit something", + ) + }) }) describe("mint", async () => { it("should mint shares to the vault", async () => {