diff --git a/contracts/savings/SavingsContract.sol b/contracts/savings/SavingsContract.sol index 0bc6539d..25844760 100644 --- a/contracts/savings/SavingsContract.sol +++ b/contracts/savings/SavingsContract.sol @@ -32,7 +32,6 @@ contract SavingsContract is InitializableToken, InitializableModule { - using SafeMath for uint256; using StableMath for uint256; @@ -40,19 +39,27 @@ contract SavingsContract is 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); + event PokerUpdated(address poker); + event FractionUpdated(uint256 fraction); + event ConnectorUpdated(address connector); + event EmergencyUpdate(); + event Poked(uint256 oldBalance, uint256 newBalance, uint256 interestDetected); + event PokedRaw(); // Rate between 'savings credits' and underlying // e.g. 1 credit (1e17) mulTruncate(exchangeRate) = underlying, starts at 10:1 // exchangeRate increases over time - uint256 public exchangeRate = 1e17; + uint256 public exchangeRate; + uint256 public colRatio; // Underlying asset is underlying IERC20 public underlying; - bool private automateInterestCollection = true; + bool private automateInterestCollection; // Yield address public poker; @@ -64,13 +71,13 @@ contract SavingsContract is uint256 constant private MAX_APY = 2e18; uint256 constant private SECONDS_IN_YEAR = 365 days; - // TODO - use constant addresses during deployment. Adds to bytecode + // TODO - Add these constants to bytecode at deploytime function initialize( address _nexus, // constant address _poker, IERC20 _underlying, // constant - string calldata _nameArg, - string calldata _symbolArg + string calldata _nameArg, // constant + string calldata _symbolArg // constant ) external initializer @@ -85,6 +92,8 @@ contract SavingsContract is poker = _poker; fraction = 2e17; + automateInterestCollection = true; + exchangeRate = 1e17; } /** @dev Only the savings managaer (pulled from Nexus) can execute this */ @@ -129,7 +138,7 @@ contract SavingsContract is // new exchange rate is relationship between _totalCredits & totalSavings // _totalCredits * exchangeRate = totalSavings // exchangeRate = totalSavings/_totalCredits - uint256 amountPerCredit = _amount.divPrecisely(totalCredits); + uint256 amountPerCredit = _calcExchangeRate(_amount, totalCredits); uint256 newExchangeRate = exchangeRate.add(amountPerCredit); exchangeRate = newExchangeRate; @@ -139,15 +148,14 @@ contract SavingsContract is /*************************************** - SAVING + DEPOSIT ****************************************/ /** * @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 - * If automation is enabled, we will first update the internal exchange rate by - * collecting any interest generated on the underlying. + * 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 issued internally */ @@ -155,17 +163,24 @@ contract SavingsContract is external returns (uint256 creditsIssued) { - return _deposit(_underlying, msg.sender); + return _deposit(_underlying, msg.sender, false); } function depositSavings(uint256 _underlying, address _beneficiary) external returns (uint256 creditsIssued) { - return _deposit(_underlying, _beneficiary); + return _deposit(_underlying, _beneficiary, false); + } + + function preDeposit(uint256 _underlying, address _beneficiary) + external + returns (uint256 creditsIssued) + { + return _deposit(_underlying, _beneficiary, true); } - function _deposit(uint256 _underlying, address _beneficiary) + function _deposit(uint256 _underlying, address _beneficiary, bool _skipCollection) internal returns (uint256 creditsIssued) { @@ -173,13 +188,15 @@ contract SavingsContract is // Collect recent interest generated by basket and update exchange rate IERC20 mAsset = underlying; - ISavingsManager(_savingsManager()).collectAndDistributeInterest(address(mAsset)); + if(!_skipCollection){ + 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); + (creditsIssued,) = _underlyingToCredits(_underlying); // add credits to balances _mint(_beneficiary, creditsIssued); @@ -187,27 +204,36 @@ contract SavingsContract is emit SavingsDeposited(_beneficiary, _underlying, creditsIssued); } - /** - * @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 - */ + + /*************************************** + REDEEM + ****************************************/ + + + // Deprecated in favour of redeemCredits function redeem(uint256 _credits) external returns (uint256 massetReturned) { require(_credits > 0, "Must withdraw something"); - massetReturned = _redeem(_credits); + (, uint256 payout) = _redeem(_credits, 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) @@ -219,7 +245,9 @@ contract SavingsContract is ISavingsManager(_savingsManager()).collectAndDistributeInterest(address(underlying)); } - return _redeem(_credits); + (, uint256 payout) = _redeem(_credits, true); + + return payout; } function redeemUnderlying(uint256 _underlying) @@ -233,37 +261,63 @@ contract SavingsContract is ISavingsManager(_savingsManager()).collectAndDistributeInterest(address(underlying)); } - uint256 requiredCredits = _underlyingToCredits(_underlying); - - uint256 returned = _redeem(requiredCredits); - require(returned == _underlying, "Did not redeem sufficiently"); + (uint256 credits, uint256 massetReturned) = _redeem(_underlying, false); + require(massetReturned == _underlying, "Invalid output"); - return requiredCredits; + return credits; } - function _redeem(uint256 _credits) + function _redeem(uint256 _amt, bool _isCreditAmt) internal - returns (uint256 massetReturned) + returns (uint256 creditsBurned, uint256 massetReturned) { - _burn(msg.sender, _credits); - - // Calc payout based on currentRatio - (uint256 amt, uint256 exchangeRate_) = _creditsToUnderlying(_credits); + // Centralise credit <> underlying calcs and minimise SLOAD count + uint256 credits_ = 0; + uint256 underlying_ = 0; + uint256 exchangeRate_ = 0; + if(_isCreditAmt){ + credits_ = _amt; + (underlying_, exchangeRate_) = _creditsToUnderlying(_amt); + } else { + underlying_ = _amt; + (credits_, exchangeRate_) = _underlyingToCredits(_amt); + } - // TODO - check the collateralisation here - // - if over fraction + 2e17, then withdraw down to fraction - // - ensure that it does not affect with the APY calculations in poke + _burn(msg.sender, credits_); // Transfer tokens from here to sender - require(underlying.transfer(msg.sender, amt), "Must send tokens"); + require(underlying.transfer(msg.sender, underlying_), "Must send tokens"); - emit CreditsRedeemed(msg.sender, _credits, amt); + CachedData memory cachedData = _cacheData(); + ConnectorStatus memory status = _getConnectorStatus(cachedData, exchangeRate_); + if(status.inConnector > status.limit){ + _poke(cachedData, false); + } - return amt; + emit CreditsRedeemed(msg.sender, credits_, underlying_); + + return (credits_, underlying_); + } + + struct ConnectorStatus { + uint256 limit; + uint256 inConnector; + } + + function _getConnectorStatus(CachedData memory _data, uint256 _exchangeRate) + internal + pure + returns (ConnectorStatus memory) + { + uint256 totalCollat = _data.totalCredits.mulTruncate(_exchangeRate); + uint256 limit = totalCollat.mulTruncate(_data.fraction.add(2e17)); + uint256 inConnector = _data.rawBalance >= totalCollat ? 0 : totalCollat.sub(_data.rawBalance); + + return ConnectorStatus(limit, inConnector); } /*************************************** - YIELD + YIELD - E ****************************************/ @@ -277,71 +331,8 @@ contract SavingsContract is external onlyPoker { - // TODO - // Consider security optimisation: lastExchangeRate vs lastBalance.. do global check rather than just checking the balance of connectors - - // 1. Verify that poke cadence is valid - uint256 currentTime = uint256(now); - uint256 timeSinceLastPoke = currentTime.sub(lastPoke); - require(timeSinceLastPoke > POKE_CADENCE, "Not enough time elapsed"); - lastPoke = currentTime; - - // 2. Check and verify new connector balance - uint256 connectorBalance = connector.checkBalance(); - uint256 lastBalance_ = lastBalance; - if(connectorBalance > 0){ - require(connectorBalance >= lastBalance_, "Invalid yield"); - _validateCollection(connectorBalance, connectorBalance.sub(lastBalance_), timeSinceLastPoke); - } - lastBalance = connectorBalance; - - // 3. Level the assets to Fraction (connector) & 100-fraction (raw) - uint256 balance = underlying.balanceOf(address(this)); - uint256 realSum = balance.add(connectorBalance); - // e.g. 1e20 * 2e17 / 1e18 = 2e19 - uint256 idealConnectorAmount = realSum.mulTruncate(fraction); - if(idealConnectorAmount > connectorBalance){ - // deposit to connector - connector.deposit(idealConnectorAmount.sub(connectorBalance)); - } else { - // withdraw from connector - connector.withdraw(connectorBalance.sub(idealConnectorAmount)); - } - - // 4. Calculate new exchangeRate - (uint256 totalCredited, uint256 exchangeRate_) = _creditsToUnderlying(totalSupply()); - if(realSum > totalCredited){ - exchangeRate = realSum.divPrecisely(totalSupply()); - } - - // emit Poked(lastBalance_, connectorBalance, ); - } - - function _validateCollection(uint256 _newBalance, uint256 _interest, uint256 _timeSinceLastCollection) - internal - pure - returns (uint256 extrapolatedAPY) - { - // 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 = _newBalance.sub(_interest); - uint256 percentageIncrease = _interest.divPrecisely(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 = - _timeSinceLastCollection.divPrecisely(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.divPrecisely(yearsSinceLastCollection); - - require(extrapolatedAPY < MAX_APY, "Interest protected from inflating past maxAPY"); + CachedData memory cachedData = _cacheData(); + _poke(cachedData, false); } function setPoker(address _newPoker) @@ -363,31 +354,120 @@ contract SavingsContract is fraction = _fraction; - emit FractionUpdated(_fraction); - } + CachedData memory cachedData = _cacheData(); + _poke(cachedData, true); - function setConnector() - external - onlyGovernor - { - // Withdraw all from previous - // deposit to new - // check that the balance is legit + emit FractionUpdated(_fraction); } + // TODO - consider delaying this + // 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); + // } + + // Should it be the case that some or all of the liquidity is trapped in function emergencyStop(uint256 _withdrawAmount) external onlyGovernor { // withdraw _withdrawAmount from connection + connector.withdraw(_withdrawAmount); // check total collateralisation of credits + CachedData memory data = _cacheData(); // set collateralisation ratio - // emit emergencyStop + _refreshExchangeRate(data.rawBalance, data.totalCredits, true); + + emit EmergencyUpdate(); + } + + + /*************************************** + YIELD - I + ****************************************/ + + function _poke(CachedData memory _data, bool _ignoreCadence) internal { + // 1. Verify that poke cadence is valid + uint256 currentTime = uint256(now); + uint256 timeSinceLastPoke = currentTime.sub(lastPoke); + require(_ignoreCadence || timeSinceLastPoke > POKE_CADENCE, "Not enough time elapsed"); + lastPoke = currentTime; + + IConnector connector_ = connector; + if(address(connector_) != address(0)){ + + // 2. Check and verify new connector balance + uint256 lastBalance_ = lastBalance; + uint256 connectorBalance = connector_.checkBalance(); + require(connectorBalance >= lastBalance_, "Invalid yield"); + if(connectorBalance > 0){ + _validateCollection(connectorBalance, connectorBalance.sub(lastBalance_), timeSinceLastPoke); + } + + // 3. Level the assets to Fraction (connector) & 100-fraction (raw) + uint256 realSum = _data.rawBalance.add(connectorBalance); + uint256 ideal = realSum.mulTruncate(_data.fraction); + if(ideal > connectorBalance){ + connector.deposit(ideal.sub(connectorBalance)); + } else { + connector.withdraw(connectorBalance.sub(ideal)); + } + + // 4i. Refresh exchange rate and emit event + lastBalance = ideal; + _refreshExchangeRate(realSum, _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(); + + } + } + + function _refreshExchangeRate(uint256 _realSum, uint256 _totalCredits, bool _ignoreValidation) internal { + (uint256 totalCredited, ) = _creditsToUnderlying(_totalCredits); + + require(_ignoreValidation || _realSum >= totalCredited, "Insufficient capital"); + uint256 newExchangeRate = _calcExchangeRate(_realSum, _totalCredits); + exchangeRate = newExchangeRate; + + emit ExchangeRateUpdated(newExchangeRate, _realSum.sub(totalCredited)); + } + + function _validateCollection(uint256 _newBalance, uint256 _interest, uint256 _timeSinceLastCollection) + internal + pure + returns (uint256 extrapolatedAPY) + { + uint256 oldSupply = _newBalance.sub(_interest); + uint256 percentageIncrease = _interest.divPrecisely(oldSupply); + + uint256 yearsSinceLastCollection = + _timeSinceLastCollection.divPrecisely(SECONDS_IN_YEAR); + + extrapolatedAPY = percentageIncrease.divPrecisely(yearsSinceLastCollection); + + require(extrapolatedAPY < MAX_APY, "Interest protected from inflating past maxAPY"); } /*************************************** - VIEWING + VIEW - E ****************************************/ function balanceOfUnderlying(address _user) external view returns (uint256 balance) { @@ -398,14 +478,30 @@ contract SavingsContract is return balanceOf(_user); } - function underlyingToCredits(uint256 _underlying) external view returns (uint256) { - return _underlyingToCredits(_underlying); + function underlyingToCredits(uint256 _underlying) external view returns (uint256 credits) { + (credits,) = _underlyingToCredits(_underlying); } function creditsToUnderlying(uint256 _credits) external view returns (uint256 amount) { (amount,) = _creditsToUnderlying(_credits); } + + /*************************************** + VIEW - I + ****************************************/ + + struct CachedData { + uint256 fraction; + uint256 rawBalance; + uint256 totalCredits; + } + + 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 @@ -413,12 +509,21 @@ contract SavingsContract is function _underlyingToCredits(uint256 _underlying) internal view - returns (uint256 credits) + 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 - credits = _underlying.divPrecisely(exchangeRate).add(1); + exchangeRate_ = exchangeRate; + credits = _underlying.divPrecisely(exchangeRate_).add(1); + } + + function _calcExchangeRate(uint256 _totalCollateral, uint256 _totalCredits) + internal + pure + returns (uint256 _exchangeRate) + { + return _totalCollateral.divPrecisely(_totalCredits); } /** diff --git a/contracts/savings/peripheral/Connector_yVault.sol b/contracts/savings/peripheral/Connector_yVault.sol index 8d03ecd6..ece45eef 100644 --- a/contracts/savings/peripheral/Connector_yVault.sol +++ b/contracts/savings/peripheral/Connector_yVault.sol @@ -45,12 +45,11 @@ contract Connector_yVault is IConnector { } function withdraw(uint256 _amt) external onlySave { - IyVault vault = IyVault(yVault); // amount = shares * sharePrice // shares = amount / sharePrice - uint256 sharePrice = vault.getPricePerFullShare(); - uint256 sharesToWithdraw = _amt.divPrecisely(sharePrice).add(1); - vault.withdraw(sharesToWithdraw); + uint256 sharePrice = IyVault(yVault).getPricePerFullShare(); + uint256 sharesToWithdraw = _amt.divPrecisely(sharePrice); + IyVault(yVault).withdraw(sharesToWithdraw); IERC20(mUSD).transfer(save, _amt); } diff --git a/contracts/z_mocks/savings/MockyvMUSD.sol b/contracts/z_mocks/savings/MockyvMUSD.sol new file mode 100644 index 00000000..156cb358 --- /dev/null +++ b/contracts/z_mocks/savings/MockyvMUSD.sol @@ -0,0 +1,63 @@ + +pragma solidity ^0.5.16; + + +import { ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; +import { ERC20Detailed } from "@openzeppelin/contracts/token/ERC20/ERC20Detailed.sol"; +import { SafeERC20, IERC20 } from "@openzeppelin/contracts/token/ERC20/SafeERC20.sol"; +import { SafeMath } from "@openzeppelin/contracts/math/SafeMath.sol"; + +contract yvMUSD is ERC20 { + using SafeERC20 for IERC20; + using SafeMath for uint256; + + IERC20 public token; + + string public name; + string public symbol; + uint8 public decimals; + + constructor (address _token) public { + name = string(abi.encodePacked("yearn ", ERC20Detailed(_token).name())); + symbol = string(abi.encodePacked("yv", ERC20Detailed(_token).symbol())); + + decimals = ERC20Detailed(_token).decimals(); + token = IERC20(_token); + } + + + function balance() public view returns (uint) { + // todo - increase balance here to increase price per share + return token.balanceOf(address(this)); + } + + function depositAll() external { + deposit(token.balanceOf(msg.sender)); + } + + function deposit(uint _amount) public { + uint _pool = balance(); + token.safeTransferFrom(msg.sender, address(this), _amount); + uint shares = 0; + if (totalSupply() == 0) { + shares = _amount; + } else { + shares = (_amount.mul(totalSupply())).div(_pool); + } + _mint(msg.sender, shares); + } + + function withdrawAll() external { + withdraw(balanceOf(msg.sender)); + } + + function withdraw(uint _shares) public { + uint r = (balance().mul(_shares)).div(totalSupply()); + _burn(msg.sender, _shares); + token.safeTransfer(msg.sender, r); + } + + function getPricePerFullShare() public view returns (uint) { + return balance().mul(1e18).div(totalSupply()); + } +} \ No newline at end of file diff --git a/contracts/z_mocks/savings/yEarn_Controller.sol b/contracts/z_mocks/savings/yEarn_Controller.sol new file mode 100644 index 00000000..f4fae6d8 --- /dev/null +++ b/contracts/z_mocks/savings/yEarn_Controller.sol @@ -0,0 +1,315 @@ +/** + *Submitted for verification at Etherscan.io on 2020-08-11 +*/ + +/** + *Submitted for verification at Etherscan.io on 2020-07-26 +*/ + +// SPDX-License-Identifier: MIT + +pragma solidity ^0.5.16; + +interface IERC20 { + function totalSupply() external view returns (uint256); + function balanceOf(address account) external view returns (uint256); + function transfer(address recipient, uint256 amount) external returns (bool); + function allowance(address owner, address spender) external view returns (uint256); + function approve(address spender, uint256 amount) external returns (bool); + function transferFrom(address sender, address recipient, uint256 amount) external returns (bool); + event Transfer(address indexed from, address indexed to, uint256 value); + event Approval(address indexed owner, address indexed spender, uint256 value); +} + +library SafeMath { + function add(uint256 a, uint256 b) internal pure returns (uint256) { + uint256 c = a + b; + require(c >= a, "SafeMath: addition overflow"); + + return c; + } + function sub(uint256 a, uint256 b) internal pure returns (uint256) { + return sub(a, b, "SafeMath: subtraction overflow"); + } + function sub(uint256 a, uint256 b, string memory errorMessage) internal pure returns (uint256) { + require(b <= a, errorMessage); + uint256 c = a - b; + + return c; + } + function mul(uint256 a, uint256 b) internal pure returns (uint256) { + if (a == 0) { + return 0; + } + + uint256 c = a * b; + require(c / a == b, "SafeMath: multiplication overflow"); + + return c; + } + function div(uint256 a, uint256 b) internal pure returns (uint256) { + return div(a, b, "SafeMath: division by zero"); + } + 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; + + return c; + } + function mod(uint256 a, uint256 b) internal pure returns (uint256) { + return mod(a, b, "SafeMath: modulo by zero"); + } + function mod(uint256 a, uint256 b, string memory errorMessage) internal pure returns (uint256) { + require(b != 0, errorMessage); + return a % b; + } +} + +library Address { + function isContract(address account) internal view returns (bool) { + bytes32 codehash; + bytes32 accountHash = 0xc5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470; + // solhint-disable-next-line no-inline-assembly + assembly { codehash := extcodehash(account) } + return (codehash != 0x0 && codehash != accountHash); + } + function toPayable(address account) internal pure returns (address payable) { + return address(uint160(account)); + } + function sendValue(address payable recipient, uint256 amount) internal { + require(address(this).balance >= amount, "Address: insufficient balance"); + + // solhint-disable-next-line avoid-call-value + (bool success, ) = recipient.call.value(amount)(""); + require(success, "Address: unable to send value, recipient may have reverted"); + } +} + +library SafeERC20 { + using SafeMath for uint256; + using Address for address; + + function safeTransfer(IERC20 token, address to, uint256 value) internal { + callOptionalReturn(token, abi.encodeWithSelector(token.transfer.selector, to, value)); + } + + function safeTransferFrom(IERC20 token, address from, address to, uint256 value) internal { + callOptionalReturn(token, abi.encodeWithSelector(token.transferFrom.selector, from, to, value)); + } + + function safeApprove(IERC20 token, address spender, uint256 value) internal { + require((value == 0) || (token.allowance(address(this), spender) == 0), + "SafeERC20: approve from non-zero to non-zero allowance" + ); + callOptionalReturn(token, abi.encodeWithSelector(token.approve.selector, spender, value)); + } + function callOptionalReturn(IERC20 token, bytes memory data) private { + require(address(token).isContract(), "SafeERC20: call to non-contract"); + + // solhint-disable-next-line avoid-low-level-calls + (bool success, bytes memory returndata) = address(token).call(data); + require(success, "SafeERC20: low-level call failed"); + + if (returndata.length > 0) { // Return data is optional + // solhint-disable-next-line max-line-length + require(abi.decode(returndata, (bool)), "SafeERC20: ERC20 operation did not succeed"); + } + } +} + +interface Strategy { + function want() external view returns (address); + function deposit() external; + function withdraw(address) external; + function withdraw(uint) external; + function withdrawAll() external returns (uint); + function balanceOf() external view returns (uint); +} + +interface Converter { + function convert(address) external returns (uint); +} + +interface OneSplitAudit { + function swap( + address fromToken, + address destToken, + uint256 amount, + uint256 minReturn, + uint256[] calldata distribution, + uint256 flags + ) + external + payable + returns(uint256 returnAmount); + + function getExpectedReturn( + address fromToken, + address destToken, + uint256 amount, + uint256 parts, + uint256 flags // See constants in IOneSplit.sol + ) + external + view + returns( + uint256 returnAmount, + uint256[] memory distribution + ); +} + +contract Controller { + using SafeERC20 for IERC20; + using Address for address; + using SafeMath for uint256; + + address public governance; + address public strategist; + + address public onesplit; + address public rewards; + mapping(address => address) public vaults; + mapping(address => address) public strategies; + mapping(address => mapping(address => address)) public converters; + + mapping(address => mapping(address => bool)) public approvedStrategies; + + uint public split = 500; + uint public constant max = 10000; + + constructor(address _rewards) public { + governance = msg.sender; + strategist = msg.sender; + onesplit = address(0x50FDA034C0Ce7a8f7EFDAebDA7Aa7cA21CC1267e); + rewards = _rewards; + } + + function setRewards(address _rewards) public { + require(msg.sender == governance, "!governance"); + rewards = _rewards; + } + + function setStrategist(address _strategist) public { + require(msg.sender == governance, "!governance"); + strategist = _strategist; + } + + function setSplit(uint _split) public { + require(msg.sender == governance, "!governance"); + split = _split; + } + + function setOneSplit(address _onesplit) public { + require(msg.sender == governance, "!governance"); + onesplit = _onesplit; + } + + function setGovernance(address _governance) public { + require(msg.sender == governance, "!governance"); + governance = _governance; + } + + function setVault(address _token, address _vault) public { + require(msg.sender == strategist || msg.sender == governance, "!strategist"); + require(vaults[_token] == address(0), "vault"); + vaults[_token] = _vault; + } + + function approveStrategy(address _token, address _strategy) public { + require(msg.sender == governance, "!governance"); + approvedStrategies[_token][_strategy] = true; + } + + function revokeStrategy(address _token, address _strategy) public { + require(msg.sender == governance, "!governance"); + approvedStrategies[_token][_strategy] = false; + } + + function setConverter(address _input, address _output, address _converter) public { + require(msg.sender == strategist || msg.sender == governance, "!strategist"); + converters[_input][_output] = _converter; + } + + function setStrategy(address _token, address _strategy) public { + require(msg.sender == strategist || msg.sender == governance, "!strategist"); + require(approvedStrategies[_token][_strategy] == true, "!approved"); + + address _current = strategies[_token]; + if (_current != address(0)) { + Strategy(_current).withdrawAll(); + } + strategies[_token] = _strategy; + } + + function earn(address _token, uint _amount) public { + address _strategy = strategies[_token]; + address _want = Strategy(_strategy).want(); + if (_want != _token) { + address converter = converters[_token][_want]; + IERC20(_token).safeTransfer(converter, _amount); + _amount = Converter(converter).convert(_strategy); + IERC20(_want).safeTransfer(_strategy, _amount); + } else { + IERC20(_token).safeTransfer(_strategy, _amount); + } + Strategy(_strategy).deposit(); + } + + function balanceOf(address _token) external view returns (uint) { + return Strategy(strategies[_token]).balanceOf(); + } + + function withdrawAll(address _token) public { + require(msg.sender == strategist || msg.sender == governance, "!strategist"); + Strategy(strategies[_token]).withdrawAll(); + } + + function inCaseTokensGetStuck(address _token, uint _amount) public { + require(msg.sender == strategist || msg.sender == governance, "!governance"); + IERC20(_token).safeTransfer(msg.sender, _amount); + } + + function inCaseStrategyTokenGetStuck(address _strategy, address _token) public { + require(msg.sender == strategist || msg.sender == governance, "!governance"); + Strategy(_strategy).withdraw(_token); + } + + function getExpectedReturn(address _strategy, address _token, uint parts) public view returns (uint expected) { + uint _balance = IERC20(_token).balanceOf(_strategy); + address _want = Strategy(_strategy).want(); + (expected,) = OneSplitAudit(onesplit).getExpectedReturn(_token, _want, _balance, parts, 0); + } + + // Only allows to withdraw non-core strategy tokens ~ this is over and above normal yield + function yearn(address _strategy, address _token, uint parts) public { + require(msg.sender == strategist || msg.sender == governance, "!governance"); + // This contract should never have value in it, but just incase since this is a public call + uint _before = IERC20(_token).balanceOf(address(this)); + Strategy(_strategy).withdraw(_token); + uint _after = IERC20(_token).balanceOf(address(this)); + if (_after > _before) { + uint _amount = _after.sub(_before); + address _want = Strategy(_strategy).want(); + uint[] memory _distribution; + uint _expected; + _before = IERC20(_want).balanceOf(address(this)); + IERC20(_token).safeApprove(onesplit, 0); + IERC20(_token).safeApprove(onesplit, _amount); + (_expected, _distribution) = OneSplitAudit(onesplit).getExpectedReturn(_token, _want, _amount, parts, 0); + OneSplitAudit(onesplit).swap(_token, _want, _amount, _expected, _distribution, 0); + _after = IERC20(_want).balanceOf(address(this)); + if (_after > _before) { + _amount = _after.sub(_before); + uint _reward = _amount.mul(split).div(max); + earn(_want, _amount.sub(_reward)); + IERC20(_want).safeTransfer(rewards, _reward); + } + } + } + + function withdraw(address _token, uint _amount) public { + require(msg.sender == vaults[_token], "!vault"); + Strategy(strategies[_token]).withdraw(_amount); + } +} \ No newline at end of file diff --git a/contracts/z_mocks/savings/yEarn_StrategyCurvemUSDVoterProxy.sol b/contracts/z_mocks/savings/yEarn_StrategyCurvemUSDVoterProxy.sol new file mode 100644 index 00000000..ae6509c0 --- /dev/null +++ b/contracts/z_mocks/savings/yEarn_StrategyCurvemUSDVoterProxy.sol @@ -0,0 +1,629 @@ +/** + *Submitted for verification at Etherscan.io on 2020-12-07 +*/ + +pragma solidity ^0.5.16; + + +/** + * @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; + } +} + +/** + * @dev Collection of functions related to the address type + */ +library Address { + /** + * @dev Returns true if `account` is a contract. + * + * [IMPORTANT] + * ==== + * It is unsafe to assume that an address for which this function returns + * false is an externally-owned account (EOA) and not a contract. + * + * Among others, `isContract` will return false for the following + * types of addresses: + * + * - an externally-owned account + * - a contract in construction + * - an address where a contract will be created + * - an address where a contract lived, but was destroyed + * ==== + */ + function isContract(address account) internal view returns (bool) { + // According to EIP-1052, 0x0 is the value returned for not-yet created accounts + // and 0xc5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470 is returned + // for accounts without code, i.e. `keccak256('')` + bytes32 codehash; + bytes32 accountHash = 0xc5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470; + // solhint-disable-next-line no-inline-assembly + assembly { codehash := extcodehash(account) } + return (codehash != accountHash && codehash != 0x0); + } + + /** + * @dev Converts an `address` into `address payable`. Note that this is + * simply a type cast: the actual underlying value is not changed. + * + * _Available since v2.4.0._ + */ + function toPayable(address account) internal pure returns (address payable) { + return address(uint160(account)); + } + + /** + * @dev Replacement for Solidity's `transfer`: sends `amount` wei to + * `recipient`, forwarding all available gas and reverting on errors. + * + * https://eips.ethereum.org/EIPS/eip-1884[EIP1884] increases the gas cost + * of certain opcodes, possibly making contracts go over the 2300 gas limit + * imposed by `transfer`, making them unable to receive funds via + * `transfer`. {sendValue} removes this limitation. + * + * https://diligence.consensys.net/posts/2019/09/stop-using-soliditys-transfer-now/[Learn more]. + * + * IMPORTANT: because control is transferred to `recipient`, care must be + * taken to not create reentrancy vulnerabilities. Consider using + * {ReentrancyGuard} or the + * https://solidity.readthedocs.io/en/v0.5.11/security-considerations.html#use-the-checks-effects-interactions-pattern[checks-effects-interactions pattern]. + * + * _Available since v2.4.0._ + */ + function sendValue(address payable recipient, uint256 amount) internal { + require(address(this).balance >= amount, "Address: insufficient balance"); + + // solhint-disable-next-line avoid-call-value + (bool success, ) = recipient.call.value(amount)(""); + require(success, "Address: unable to send value, recipient may have reverted"); + } +} + +/** + * @title SafeERC20 + * @dev Wrappers around ERC20 operations that throw on failure (when the token + * contract returns false). Tokens that return no value (and instead revert or + * throw on failure) are also supported, non-reverting calls are assumed to be + * successful. + * To use this library you can add a `using SafeERC20 for ERC20;` statement to your contract, + * which allows you to call the safe operations as `token.safeTransfer(...)`, etc. + */ +library SafeERC20 { + using SafeMath for uint256; + using Address for address; + + function safeTransfer(IERC20 token, address to, uint256 value) internal { + callOptionalReturn(token, abi.encodeWithSelector(token.transfer.selector, to, value)); + } + + function safeTransferFrom(IERC20 token, address from, address to, uint256 value) internal { + callOptionalReturn(token, abi.encodeWithSelector(token.transferFrom.selector, from, to, value)); + } + + function safeApprove(IERC20 token, address spender, uint256 value) internal { + // safeApprove should only be called when setting an initial allowance, + // or when resetting it to zero. To increase and decrease it, use + // 'safeIncreaseAllowance' and 'safeDecreaseAllowance' + // solhint-disable-next-line max-line-length + require((value == 0) || (token.allowance(address(this), spender) == 0), + "SafeERC20: approve from non-zero to non-zero allowance" + ); + callOptionalReturn(token, abi.encodeWithSelector(token.approve.selector, spender, value)); + } + + function safeIncreaseAllowance(IERC20 token, address spender, uint256 value) internal { + uint256 newAllowance = token.allowance(address(this), spender).add(value); + callOptionalReturn(token, abi.encodeWithSelector(token.approve.selector, spender, newAllowance)); + } + + function safeDecreaseAllowance(IERC20 token, address spender, uint256 value) internal { + uint256 newAllowance = token.allowance(address(this), spender).sub(value, "SafeERC20: decreased allowance below zero"); + callOptionalReturn(token, abi.encodeWithSelector(token.approve.selector, spender, newAllowance)); + } + + /** + * @dev Imitates a Solidity high-level call (i.e. a regular function call to a contract), relaxing the requirement + * on the return value: the return value is optional (but if data is returned, it must not be false). + * @param token The token targeted by the call. + * @param data The call data (encoded using abi.encode or one of its variants). + */ + function callOptionalReturn(IERC20 token, bytes memory data) private { + // We need to perform a low level call here, to bypass Solidity's return data size checking mechanism, since + // we're implementing it ourselves. + + // A Solidity high level call has three parts: + // 1. The target address is checked to verify it contains contract code + // 2. The call itself is made, and success asserted + // 3. The return value is decoded, which in turn checks the size of the returned data. + // solhint-disable-next-line max-line-length + require(address(token).isContract(), "SafeERC20: call to non-contract"); + + // solhint-disable-next-line avoid-low-level-calls + (bool success, bytes memory returndata) = address(token).call(data); + require(success, "SafeERC20: low-level call failed"); + + if (returndata.length > 0) { // Return data is optional + // solhint-disable-next-line max-line-length + require(abi.decode(returndata, (bool)), "SafeERC20: ERC20 operation did not succeed"); + } + } +} +interface IController { + function withdraw(address, uint256) external; + function balanceOf(address) external view returns (uint256); + function earn(address, uint256) external; + function want(address) external view returns (address); + function rewards() external view returns (address); + function vaults(address) external view returns (address); + function strategies(address) external view returns (address); +} +interface Gauge { + function deposit(uint256) external; + function balanceOf(address) external view returns (uint256); + function withdraw(uint256) external; +} +interface Mintr { + function mint(address) external; +} +interface Uni { + function swapExactTokensForTokens( + uint256, + uint256, + address[] calldata, + address, + uint256 + ) external; +} +interface ICurveFi { + function add_liquidity( + uint256[4] calldata amounts, + uint256 min_mint_amount + ) external; +} + +interface VoterProxy { + function withdraw( + address _gauge, + address _token, + uint256 _amount + ) external returns (uint256); + function balanceOf(address _gauge) external view returns (uint256); + function withdrawAll(address _gauge, address _token) external returns (uint256); + function deposit(address _gauge, address _token) external; + function harvest(address _gauge) external; + function lock() external; +} + +contract StrategyCurvemUSDVoterProxy { + using SafeERC20 for IERC20; + using Address for address; + using SafeMath for uint256; + + address public constant want = address(0x1AEf73d49Dedc4b1778d0706583995958Dc862e6); + address public constant crv = address(0xD533a949740bb3306d119CC777fa900bA034cd52); + + address public constant curve = address(0x78CF256256C8089d68Cde634Cf7cDEFb39286470); + address public constant gauge = address(0x5f626c30EC1215f4EdCc9982265E8b1F411D1352); + address public constant voter = address(0xF147b8125d2ef93FB6965Db97D6746952a133934); + + address public constant uniswap = address(0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D); + address public constant sushiswap = address(0xd9e1cE17f2641f24aE83637ab66a2cca9C378B9F); + address public constant dai = address(0x6B175474E89094C44Da98b954EedeAC495271d0F); + address public constant weth = address(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2); // used for crv <> weth <> dai route + + uint256 public keepCRV = 1000; + uint256 public treasuryFee = 1000; + uint256 public strategistReward = 1000; + uint256 public withdrawalFee = 0; + uint256 public constant FEE_DENOMINATOR = 10000; + + address public proxy; + address public dex; + + address public governance; + address public controller; + address public strategist; + address public keeper; + + uint256 public earned; // lifetime strategy earnings denominated in `want` token + + event Harvested(uint256 wantEarned, uint256 lifetimeEarned); + + constructor(address _controller) public { + governance = msg.sender; + strategist = msg.sender; + keeper = msg.sender; + controller = _controller; + // standardize constructor + proxy = address(0xC17ADf949f524213a540609c386035D7D685B16F); + dex = sushiswap; + } + + function getName() external pure returns (string memory) { + return "StrategyCurvemUSDVoterProxy"; + } + + function setStrategist(address _strategist) external { + require(msg.sender == strategist || msg.sender == governance, "!authorized"); + strategist = _strategist; + } + + function setKeeper(address _keeper) external { + require(msg.sender == strategist || msg.sender == governance, "!authorized"); + keeper = _keeper; + } + + function setKeepCRV(uint256 _keepCRV) external { + require(msg.sender == governance, "!governance"); + keepCRV = _keepCRV; + } + + function setWithdrawalFee(uint256 _withdrawalFee) external { + require(msg.sender == governance, "!governance"); + withdrawalFee = _withdrawalFee; + } + + function setTreasuryFee(uint256 _treasuryFee) external { + require(msg.sender == governance, "!governance"); + treasuryFee = _treasuryFee; + } + + function setStrategistReward(uint256 _strategistReward) external { + require(msg.sender == governance, "!governance"); + strategistReward = _strategistReward; + } + + function setProxy(address _proxy) external { + require(msg.sender == governance, "!governance"); + proxy = _proxy; + } + + function switchDex(bool isUniswap) external { + require(msg.sender == strategist || msg.sender == governance, "!authorized"); + if (isUniswap) { + dex = uniswap; + } else { + dex = sushiswap; + } + } + + function deposit() public { + uint256 _want = IERC20(want).balanceOf(address(this)); + if (_want > 0) { + IERC20(want).safeTransfer(proxy, _want); + VoterProxy(proxy).deposit(gauge, want); + } + } + + // Controller only function for creating additional rewards from dust + function withdraw(IERC20 _asset) external returns (uint256 balance) { + require(msg.sender == controller, "!controller"); + require(want != address(_asset), "want"); + require(crv != address(_asset), "crv"); + require(dai != address(_asset), "dai"); + balance = _asset.balanceOf(address(this)); + _asset.safeTransfer(controller, balance); + } + + // Withdraw partial funds, normally used with a vault withdrawal + function withdraw(uint256 _amount) external { + require(msg.sender == controller, "!controller"); + uint256 _balance = IERC20(want).balanceOf(address(this)); + if (_balance < _amount) { + _amount = _withdrawSome(_amount.sub(_balance)); + _amount = _amount.add(_balance); + } + + uint256 _fee = _amount.mul(withdrawalFee).div(FEE_DENOMINATOR); + + IERC20(want).safeTransfer(IController(controller).rewards(), _fee); + address _vault = IController(controller).vaults(address(want)); + require(_vault != address(0), "!vault"); // additional protection so we don't burn the funds + IERC20(want).safeTransfer(_vault, _amount.sub(_fee)); + } + + function _withdrawSome(uint256 _amount) internal returns (uint256) { + return VoterProxy(proxy).withdraw(gauge, want, _amount); + } + + // Withdraw all funds, normally used when migrating strategies + function withdrawAll() external returns (uint256 balance) { + require(msg.sender == controller, "!controller"); + _withdrawAll(); + + balance = IERC20(want).balanceOf(address(this)); + + address _vault = IController(controller).vaults(address(want)); + require(_vault != address(0), "!vault"); // additional protection so we don't burn the funds + IERC20(want).safeTransfer(_vault, balance); + } + + function _withdrawAll() internal { + VoterProxy(proxy).withdrawAll(gauge, want); + } + + function harvest() public { + require(msg.sender == keeper || msg.sender == strategist || msg.sender == governance, "!keepers"); + VoterProxy(proxy).harvest(gauge); + uint256 _crv = IERC20(crv).balanceOf(address(this)); + if (_crv > 0) { + uint256 _keepCRV = _crv.mul(keepCRV).div(FEE_DENOMINATOR); + IERC20(crv).safeTransfer(voter, _keepCRV); + _crv = _crv.sub(_keepCRV); + + IERC20(crv).safeApprove(dex, 0); + IERC20(crv).safeApprove(dex, _crv); + + address[] memory path = new address[](3); + path[0] = crv; + path[1] = weth; + path[2] = dai; + + Uni(dex).swapExactTokensForTokens(_crv, uint256(0), path, address(this), now.add(1800)); + } + uint256 _dai = IERC20(dai).balanceOf(address(this)); + if (_dai > 0) { + IERC20(dai).safeApprove(curve, 0); + IERC20(dai).safeApprove(curve, _dai); + ICurveFi(curve).add_liquidity([0, _dai, 0, 0], 0); + } + uint256 _want = IERC20(want).balanceOf(address(this)); + if (_want > 0) { + uint256 _fee = _want.mul(treasuryFee).div(FEE_DENOMINATOR); + uint256 _reward = _want.mul(strategistReward).div(FEE_DENOMINATOR); + IERC20(want).safeTransfer(IController(controller).rewards(), _fee); + IERC20(want).safeTransfer(strategist, _reward); + deposit(); + } + VoterProxy(proxy).lock(); + earned = earned.add(_want); + emit Harvested(_want, earned); + } + + function balanceOfWant() public view returns (uint256) { + return IERC20(want).balanceOf(address(this)); + } + + function balanceOfPool() public view returns (uint256) { + return VoterProxy(proxy).balanceOf(gauge); + } + + function balanceOf() public view returns (uint256) { + return balanceOfWant().add(balanceOfPool()); + } + + function setGovernance(address _governance) external { + require(msg.sender == governance, "!governance"); + governance = _governance; + } + + function setController(address _controller) external { + require(msg.sender == governance, "!governance"); + controller = _controller; + } +} \ No newline at end of file diff --git a/contracts/z_mocks/savings/yEarn_Vault.sol b/contracts/z_mocks/savings/yEarn_Vault.sol new file mode 100644 index 00000000..9c27775c --- /dev/null +++ b/contracts/z_mocks/savings/yEarn_Vault.sol @@ -0,0 +1,339 @@ +/** + *Submitted for verification at Etherscan.io on 2020-11-07 +*/ + +pragma solidity ^0.5.16; + +interface IERC20 { + function totalSupply() external view returns (uint256); + function balanceOf(address account) external view returns (uint256); + function transfer(address recipient, uint256 amount) external returns (bool); + function allowance(address owner, address spender) external view returns (uint256); + function approve(address spender, uint256 amount) external returns (bool); + function transferFrom(address sender, address recipient, uint256 amount) external returns (bool); + event Transfer(address indexed from, address indexed to, uint256 value); + event Approval(address indexed owner, address indexed spender, uint256 value); +} + +interface IDetailed { + function name() external view returns (string memory); + function symbol() external view returns (string memory); + function decimals() external view returns (uint8); +} + +contract Context { + 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; + } +} + +contract ERC20 is Context, IERC20 { + using SafeMath for uint256; + + mapping (address => uint256) private _balances; + + mapping (address => mapping (address => uint256)) private _allowances; + + uint256 private _totalSupply; + function totalSupply() public view returns (uint256) { + return _totalSupply; + } + function balanceOf(address account) public view returns (uint256) { + return _balances[account]; + } + function transfer(address recipient, uint256 amount) public returns (bool) { + _transfer(_msgSender(), recipient, amount); + return true; + } + function allowance(address owner, address spender) public view returns (uint256) { + return _allowances[owner][spender]; + } + function approve(address spender, uint256 amount) public returns (bool) { + _approve(_msgSender(), spender, amount); + return true; + } + 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; + } + function increaseAllowance(address spender, uint256 addedValue) public returns (bool) { + _approve(_msgSender(), spender, _allowances[_msgSender()][spender].add(addedValue)); + return true; + } + function decreaseAllowance(address spender, uint256 subtractedValue) public returns (bool) { + _approve(_msgSender(), spender, _allowances[_msgSender()][spender].sub(subtractedValue, "ERC20: decreased allowance below zero")); + return true; + } + 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); + } + 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); + } + 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); + } + 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); + } + function _burnFrom(address account, uint256 amount) internal { + _burn(account, amount); + _approve(account, _msgSender(), _allowances[account][_msgSender()].sub(amount, "ERC20: burn amount exceeds allowance")); + } +} + +library SafeMath { + function add(uint256 a, uint256 b) internal pure returns (uint256) { + uint256 c = a + b; + require(c >= a, "SafeMath: addition overflow"); + + return c; + } + function sub(uint256 a, uint256 b) internal pure returns (uint256) { + return sub(a, b, "SafeMath: subtraction overflow"); + } + function sub(uint256 a, uint256 b, string memory errorMessage) internal pure returns (uint256) { + require(b <= a, errorMessage); + uint256 c = a - b; + + return c; + } + function mul(uint256 a, uint256 b) internal pure returns (uint256) { + if (a == 0) { + return 0; + } + + uint256 c = a * b; + require(c / a == b, "SafeMath: multiplication overflow"); + + return c; + } + function div(uint256 a, uint256 b) internal pure returns (uint256) { + return div(a, b, "SafeMath: division by zero"); + } + 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; + + return c; + } + function mod(uint256 a, uint256 b) internal pure returns (uint256) { + return mod(a, b, "SafeMath: modulo by zero"); + } + function mod(uint256 a, uint256 b, string memory errorMessage) internal pure returns (uint256) { + require(b != 0, errorMessage); + return a % b; + } +} + +library Address { + function isContract(address account) internal view returns (bool) { + bytes32 codehash; + bytes32 accountHash = 0xc5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470; + // solhint-disable-next-line no-inline-assembly + assembly { codehash := extcodehash(account) } + return (codehash != 0x0 && codehash != accountHash); + } + function toPayable(address account) internal pure returns (address payable) { + return address(uint160(account)); + } + function sendValue(address payable recipient, uint256 amount) internal { + require(address(this).balance >= amount, "Address: insufficient balance"); + + // solhint-disable-next-line avoid-call-value + (bool success, ) = recipient.call.value(amount)(""); + require(success, "Address: unable to send value, recipient may have reverted"); + } +} + +library SafeERC20 { + using SafeMath for uint256; + using Address for address; + + function safeTransfer(IERC20 token, address to, uint256 value) internal { + callOptionalReturn(token, abi.encodeWithSelector(token.transfer.selector, to, value)); + } + + function safeTransferFrom(IERC20 token, address from, address to, uint256 value) internal { + callOptionalReturn(token, abi.encodeWithSelector(token.transferFrom.selector, from, to, value)); + } + + function safeApprove(IERC20 token, address spender, uint256 value) internal { + require((value == 0) || (token.allowance(address(this), spender) == 0), + "SafeERC20: approve from non-zero to non-zero allowance" + ); + callOptionalReturn(token, abi.encodeWithSelector(token.approve.selector, spender, value)); + } + + function safeIncreaseAllowance(IERC20 token, address spender, uint256 value) internal { + uint256 newAllowance = token.allowance(address(this), spender).add(value); + callOptionalReturn(token, abi.encodeWithSelector(token.approve.selector, spender, newAllowance)); + } + + function safeDecreaseAllowance(IERC20 token, address spender, uint256 value) internal { + uint256 newAllowance = token.allowance(address(this), spender).sub(value, "SafeERC20: decreased allowance below zero"); + callOptionalReturn(token, abi.encodeWithSelector(token.approve.selector, spender, newAllowance)); + } + function callOptionalReturn(IERC20 token, bytes memory data) private { + require(address(token).isContract(), "SafeERC20: call to non-contract"); + + // solhint-disable-next-line avoid-low-level-calls + (bool success, bytes memory returndata) = address(token).call(data); + require(success, "SafeERC20: low-level call failed"); + + if (returndata.length > 0) { // Return data is optional + // solhint-disable-next-line max-line-length + require(abi.decode(returndata, (bool)), "SafeERC20: ERC20 operation did not succeed"); + } + } +} + +interface Controller { + function withdraw(address, uint) external; + function balanceOf(address) external view returns (uint); + function earn(address, uint) external; +} + +contract yVault is ERC20 { + using SafeERC20 for IERC20; + using Address for address; + using SafeMath for uint256; + + IERC20 public token; + + uint public min = 9990; + uint public constant max = 10000; + + address public governance; + address public controller; + + string public name; + string public symbol; + uint8 public decimals; + + constructor (address _token, address _controller) public { + name = string(abi.encodePacked("yearn ", IDetailed(_token).name())); + symbol = string(abi.encodePacked("yv", IDetailed(_token).symbol())); + decimals = IDetailed(_token).decimals(); + + token = IERC20(_token); + governance = msg.sender; + controller = _controller; + } + + function setName(string calldata _name) external { + require(msg.sender == governance, "!governance"); + name = _name; + } + + function setSymbol(string calldata _symbol) external { + require(msg.sender == governance, "!governance"); + symbol = _symbol; + } + + function balance() public view returns (uint) { + return token.balanceOf(address(this)) + .add(Controller(controller).balanceOf(address(token))); + } + + function setMin(uint _min) external { + require(msg.sender == governance, "!governance"); + min = _min; + } + + function setGovernance(address _governance) public { + require(msg.sender == governance, "!governance"); + governance = _governance; + } + + function setController(address _controller) public { + require(msg.sender == governance, "!governance"); + controller = _controller; + } + + // Custom logic in here for how much the vault allows to be borrowed + // Sets minimum required on-hand to keep small withdrawals cheap + function available() public view returns (uint) { + return token.balanceOf(address(this)).mul(min).div(max); + } + + function earn() public { + uint _bal = available(); + token.safeTransfer(controller, _bal); + Controller(controller).earn(address(token), _bal); + } + + function depositAll() external { + deposit(token.balanceOf(msg.sender)); + } + + function deposit(uint _amount) public { + uint _pool = balance(); + uint _before = token.balanceOf(address(this)); + token.safeTransferFrom(msg.sender, address(this), _amount); + uint _after = token.balanceOf(address(this)); + _amount = _after.sub(_before); // Additional check for deflationary tokens + uint shares = 0; + if (totalSupply() == 0) { + shares = _amount; + } else { + shares = (_amount.mul(totalSupply())).div(_pool); + } + _mint(msg.sender, shares); + } + + function withdrawAll() external { + withdraw(balanceOf(msg.sender)); + } + + // No rebalance implementation for lower fees and faster swaps + function withdraw(uint _shares) public { + uint r = (balance().mul(_shares)).div(totalSupply()); + _burn(msg.sender, _shares); + + // Check balance + uint b = token.balanceOf(address(this)); + if (b < r) { + uint _withdraw = r.sub(b); + Controller(controller).withdraw(address(token), _withdraw); + uint _after = token.balanceOf(address(this)); + uint _diff = _after.sub(b); + if (_diff < _withdraw) { + r = b.add(_diff); + } + } + + token.safeTransfer(msg.sender, r); + } + + function getPricePerFullShare() public view returns (uint) { + return balance().mul(1e18).div(totalSupply()); + } +} \ No newline at end of file diff --git a/test/savings/TestSavingsContract.spec.ts b/test/savings/TestSavingsContract.spec.ts index b567a48d..18190464 100644 --- a/test/savings/TestSavingsContract.spec.ts +++ b/test/savings/TestSavingsContract.spec.ts @@ -385,34 +385,32 @@ contract("SavingsContract", async (accounts) => { expect(before.exchangeRate).to.bignumber.equal(after.exchangeRate); }); - // it("should deposit interest when some credits exist", async () => { - // const TWENTY_TOKENS = TEN_EXACT.muln(2)); - - // // Deposit to SavingsContract - // await masset.approve(savingsContract.address, TEN_EXACT); - // await savingsContract.automateInterestCollectionFlag(false, { from: sa.governor }); - // await savingsContract.methods["depositSavings(uint256)"](TEN_EXACT); - - // const balanceBefore = await masset.balanceOf(savingsContract.address); - - // // Deposit Interest - // const tx = await savingsContract.depositInterest(TEN_EXACT, { - // from: savingsManagerAccount, - // }); - // expectEvent.inLogs(tx.logs, "ExchangeRateUpdated", { - // newExchangeRate: TWENTY_TOKENS.mul(initialExchangeRate).div(TEN_EXACT), - // interestCollected: TEN_EXACT, - // }); - - // const exchangeRateAfter = await savingsContract.exchangeRate(); - // const balanceAfter = await masset.balanceOf(savingsContract.address); - // expect(TWENTY_TOKENS).to.bignumber.equal(await savingsContract.totalSavings()); - // expect(balanceBefore.add(TEN_EXACT)).to.bignumber.equal(balanceAfter); - - // // exchangeRate should change - // const expectedExchangeRate = TWENTY_TOKENS.mul(initialExchangeRate).div(TEN_EXACT); - // expect(expectedExchangeRate).to.bignumber.equal(exchangeRateAfter); - // }); + it("should deposit interest when some credits exist", async () => { + const TWENTY_TOKENS = TEN_EXACT.muln(2); + + // Deposit to SavingsContract + await masset.approve(savingsContract.address, TEN_EXACT); + await savingsContract.preDeposit(TEN_EXACT, sa.default); + + const balanceBefore = await masset.balanceOf(savingsContract.address); + + // Deposit Interest + const tx = await savingsContract.depositInterest(TEN_EXACT, { + from: savingsManagerAccount, + }); + const expectedExchangeRate = TWENTY_TOKENS.mul(fullScale).div(HUNDRED).subn(1); + expectEvent.inLogs(tx.logs, "ExchangeRateUpdated", { + newExchangeRate: expectedExchangeRate, + interestCollected: TEN_EXACT, + }); + + const exchangeRateAfter = await savingsContract.exchangeRate(); + const balanceAfter = await masset.balanceOf(savingsContract.address); + expect(balanceBefore.add(TEN_EXACT)).to.bignumber.equal(balanceAfter); + + // exchangeRate should change + expect(expectedExchangeRate).to.bignumber.equal(exchangeRateAfter); + }); }); }); @@ -497,92 +495,102 @@ contract("SavingsContract", async (accounts) => { }); }); + describe("testing poking", () => { + it("should work correctly when changing connector"); + it("should work correctly after changing from no connector to connector"); + it("should work correctly after changing fraction"); + }); + + describe("testing emergency stop", () => { + it("should factor in to the new exchange rate, working for deposits, redeems etc"); + }); + context("performing multiple operations from multiple addresses in sequence", async () => { describe("depositing, collecting interest and then depositing/withdrawing", async () => { before(async () => { await createNewSavingsContract(false); }); - // 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, saver2deposit); - // await masset.transfer(saver3, saver3deposit); - // await masset.transfer(saver4, saver4deposit); - // await masset.approve(savingsContract.address, MAX_UINT256, { from: saver1 }); - // await masset.approve(savingsContract.address, MAX_UINT256, { from: saver2 }); - // await masset.approve(savingsContract.address, MAX_UINT256, { from: saver3 }); - // await masset.approve(savingsContract.address, MAX_UINT256, { from: saver4 }); - - // // Should be a fresh balance sheet - // const stateBefore = await getBalances(savingsContract, sa.default); - // expect(stateBefore.exchangeRate).to.bignumber.equal(initialExchangeRate); - // expect(stateBefore.totalSavings).to.bignumber.equal(new BN(0)); - - // // 1.0 user 1 deposits - // // interest remains unassigned and exchange rate unmoved - // await masset.setAmountForCollectInterest(interestToReceive1); - // await time.increase(ONE_DAY); - // await savingsContract.methods["depositSavings(uint256)"](saver1deposit, { - // from: saver1, - // }); - // await savingsContract.poke(); - // const state1 = await getBalances(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 time.increase(ONE_DAY); - // await savingsContract.methods["depositSavings(uint256)"](saver2deposit, { - // from: saver2, - // }); - // const state2 = await getBalances(savingsContract, saver2); - // // 3.0 user 3 deposits - // // interest rate benefits users 1 and 2 - // await masset.setAmountForCollectInterest(interestToReceive3); - // await time.increase(ONE_DAY); - // await savingsContract.methods["depositSavings(uint256)"](saver3deposit, { - // from: saver3, - // }); - // const state3 = await getBalances(savingsContract, saver3); - // // 4.0 user 1 withdraws all her credits - // await savingsContract.redeem(state1.userCredits, { from: saver1 }); - // const state4 = await getBalances(savingsContract, saver1); - // expect(state4.userCredits).bignumber.eq(new BN(0)); - // expect(state4.totalSupply).bignumber.eq(state3.totalSupply.sub(state1.userCredits)); - // expect(state4.exchangeRate).bignumber.eq(state3.exchangeRate); - // assertBNClose( - // state4.totalSavings, - // creditsToUnderlying(state4.totalSupply, state4.exchangeRate), - // new BN(100000), - // ); - // // 5.0 user 4 deposits - // // interest rate benefits users 2 and 3 - // await masset.setAmountForCollectInterest(interestToReceive4); - // await time.increase(ONE_DAY); - // await savingsContract.methods["depositSavings(uint256)"](saver4deposit, { - // from: saver4, - // }); - // const state5 = await getBalances(savingsContract, saver4); - // // 6.0 users 2, 3, and 4 withdraw all their tokens - // await savingsContract.redeem(state2.userCredits, { from: saver2 }); - // await savingsContract.redeem(state3.userCredits, { from: saver3 }); - // await savingsContract.redeem(state5.userCredits, { from: saver4 }); - // }); + 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, saver2deposit); + await masset.transfer(saver3, saver3deposit); + await masset.transfer(saver4, saver4deposit); + await masset.approve(savingsContract.address, MAX_UINT256, { from: saver1 }); + await masset.approve(savingsContract.address, MAX_UINT256, { from: saver2 }); + await masset.approve(savingsContract.address, MAX_UINT256, { from: saver3 }); + await masset.approve(savingsContract.address, MAX_UINT256, { from: saver4 }); + + // Should be a fresh balance sheet + const stateBefore = await getBalances(savingsContract, sa.default); + expect(stateBefore.exchangeRate).to.bignumber.equal(initialExchangeRate); + expect(stateBefore.totalSavings).to.bignumber.equal(new BN(0)); + + // 1.0 user 1 deposits + // interest remains unassigned and exchange rate unmoved + await masset.setAmountForCollectInterest(interestToReceive1); + await time.increase(ONE_DAY); + await savingsContract.methods["depositSavings(uint256)"](saver1deposit, { + from: saver1, + }); + await savingsContract.poke(); + const state1 = await getBalances(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 time.increase(ONE_DAY); + await savingsContract.methods["depositSavings(uint256)"](saver2deposit, { + from: saver2, + }); + const state2 = await getBalances(savingsContract, saver2); + // 3.0 user 3 deposits + // interest rate benefits users 1 and 2 + await masset.setAmountForCollectInterest(interestToReceive3); + await time.increase(ONE_DAY); + await savingsContract.methods["depositSavings(uint256)"](saver3deposit, { + from: saver3, + }); + const state3 = await getBalances(savingsContract, saver3); + // 4.0 user 1 withdraws all her credits + await savingsContract.redeem(state1.userCredits, { from: saver1 }); + const state4 = await getBalances(savingsContract, saver1); + expect(state4.userCredits).bignumber.eq(new BN(0)); + expect(state4.totalSupply).bignumber.eq(state3.totalSupply.sub(state1.userCredits)); + expect(state4.exchangeRate).bignumber.eq(state3.exchangeRate); + assertBNClose( + state4.totalSavings, + creditsToUnderlying(state4.totalSupply, state4.exchangeRate), + new BN(100000), + ); + // 5.0 user 4 deposits + // interest rate benefits users 2 and 3 + await masset.setAmountForCollectInterest(interestToReceive4); + await time.increase(ONE_DAY); + await savingsContract.methods["depositSavings(uint256)"](saver4deposit, { + from: saver4, + }); + const state5 = await getBalances(savingsContract, saver4); + // 6.0 users 2, 3, and 4 withdraw all their tokens + await savingsContract.redeem(state2.userCredits, { from: saver2 }); + await savingsContract.redeem(state3.userCredits, { from: saver3 }); + await savingsContract.redeem(state5.userCredits, { from: saver4 }); + }); }); });