diff --git a/contracts/mainnet/mstable/events.sol b/contracts/mainnet/mstable/events.sol new file mode 100644 index 00000000..7cabfddc --- /dev/null +++ b/contracts/mainnet/mstable/events.sol @@ -0,0 +1,13 @@ +pragma solidity ^0.7.6; + +contract Events { + event LogDeposit(address token, uint256 amount, address path); + event LogWithdraw(address token, uint256 amount, address path); + event LogClaimRewards(address token, uint256 amount); + event LogSwap( + address from, + address to, + uint256 amountIn, + uint256 amountOut + ); +} diff --git a/contracts/mainnet/mstable/helpers.sol b/contracts/mainnet/mstable/helpers.sol new file mode 100644 index 00000000..f98c9688 --- /dev/null +++ b/contracts/mainnet/mstable/helpers.sol @@ -0,0 +1,13 @@ +pragma solidity ^0.7.6; + +import { DSMath } from "../common/math.sol"; +import { Basic } from "../common/basic.sol"; + +abstract contract Helpers is DSMath, Basic { + address internal constant mUsdToken = + 0xe2f2a5C287993345a840Db3B0845fbC70f5935a5; + address internal constant imUsdToken = + 0x30647a72Dc82d7Fbb1123EA74716aB8A317Eac19; + address internal constant imUsdVault = + 0x78BefCa7de27d07DC6e71da295Cc2946681A6c7B; +} diff --git a/contracts/mainnet/mstable/interface.sol b/contracts/mainnet/mstable/interface.sol new file mode 100644 index 00000000..a4e3531e --- /dev/null +++ b/contracts/mainnet/mstable/interface.sol @@ -0,0 +1,381 @@ +pragma solidity ^0.7.6; + +interface IMasset { + function mint( + address _input, + uint256 _inputQuantity, + uint256 _minOutputQuantity, + address _recipient + ) external returns (uint256 mintOutput); + + function mintMulti( + address[] calldata _inputs, + uint256[] calldata _inputQuantities, + uint256 _minOutputQuantity, + address _recipient + ) external returns (uint256 mintOutput); + + function getMintOutput(address _input, uint256 _inputQuantity) + external + view + returns (uint256 mintOutput); + + function getMintMultiOutput( + address[] calldata _inputs, + uint256[] calldata _inputQuantities + ) external view returns (uint256 mintOutput); + + function swap( + address _input, + address _output, + uint256 _inputQuantity, + uint256 _minOutputQuantity, + address _recipient + ) external returns (uint256 swapOutput); + + function getSwapOutput( + address _input, + address _output, + uint256 _inputQuantity + ) external view returns (uint256 swapOutput); + + function redeem( + address _output, + uint256 _mAssetQuantity, + uint256 _minOutputQuantity, + address _recipient + ) external returns (uint256 outputQuantity); + + function redeemMasset( + uint256 _mAssetQuantity, + uint256[] calldata _minOutputQuantities, + address _recipient + ) external returns (uint256[] memory outputQuantities); + + function redeemExactBassets( + address[] calldata _outputs, + uint256[] calldata _outputQuantities, + uint256 _maxMassetQuantity, + address _recipient + ) external returns (uint256 mAssetRedeemed); + + function getRedeemOutput(address _output, uint256 _mAssetQuantity) + external + view + returns (uint256 bAssetOutput); + + function getRedeemExactBassetsOutput( + address[] calldata _outputs, + uint256[] calldata _outputQuantities + ) external view returns (uint256 mAssetAmount); + + // Views + // This return an index, could be used to check if it's part of the basket + function bAssetIndexes(address) external view returns (uint8); + + function getPrice() external view returns (uint256 price, uint256 k); +} + +interface ISavingsContractV2 { + 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 +} + +interface IBoostedSavingsVault { + /** + * @dev Stakes a given amount of the StakingToken for the sender + * @param _amount Units of StakingToken + */ + function stake(uint256 _amount) external; + + /** + * @dev Stakes a given amount of the StakingToken for a given beneficiary + * @param _beneficiary Staked tokens are credited to this address + * @param _amount Units of StakingToken + */ + function stake(address _beneficiary, uint256 _amount) external; + + /** + * @dev Withdraws stake from pool and claims any unlocked rewards. + * Note, this function is costly - the args for _claimRewards + * should be determined off chain and then passed to other fn + */ + function exit() external; + + /** + * @dev Withdraws stake from pool and claims any unlocked rewards. + * @param _first Index of the first array element to claim + * @param _last Index of the last array element to claim + */ + function exit(uint256 _first, uint256 _last) external; + + /** + * @dev Withdraws given stake amount from the pool + * @param _amount Units of the staked token to withdraw + */ + function withdraw(uint256 _amount) external; + + /** + * @dev Claims only the tokens that have been immediately unlocked, not including + * those that are in the lockers. + */ + function claimReward() external; + + /** + * @dev Claims all unlocked rewards for sender. + * Note, this function is costly - the args for _claimRewards + * should be determined off chain and then passed to other fn + */ + function claimRewards() external; + + /** + * @dev Claims all unlocked rewards for sender. Both immediately unlocked + * rewards and also locked rewards past their time lock. + * @param _first Index of the first array element to claim + * @param _last Index of the last array element to claim + */ + function claimRewards(uint256 _first, uint256 _last) external; + + /** + * @dev Pokes a given account to reset the boost + */ + function pokeBoost(address _account) external; + + /** + * @dev Gets the RewardsToken + */ + function getRewardToken() external view returns (IERC20); + + /** + * @dev Gets the last applicable timestamp for this reward period + */ + function lastTimeRewardApplicable() external view returns (uint256); + + /** + * @dev Calculates the amount of unclaimed rewards per token since last update, + * and sums with stored to give the new cumulative reward per token + * @return 'Reward' per staked token + */ + function rewardPerToken() external view returns (uint256); + + /** + * @dev Returned the units of IMMEDIATELY claimable rewards a user has to receive. Note - this + * does NOT include the majority of rewards which will be locked up. + * @param _account User address + * @return Total reward amount earned + */ + function earned(address _account) external view returns (uint256); + + /** + * @dev Calculates all unclaimed reward data, finding both immediately unlocked rewards + * and those that have passed their time lock. + * @param _account User address + * @return amount Total units of unclaimed rewards + * @return first Index of the first userReward that has unlocked + * @return last Index of the last userReward that has unlocked + */ + function unclaimedRewards(address _account) + external + view + returns ( + uint256 amount, + uint256 first, + uint256 last + ); +} + +abstract contract IFeederPool { + // Mint + function mint( + address _input, + uint256 _inputQuantity, + uint256 _minOutputQuantity, + address _recipient + ) external virtual returns (uint256 mintOutput); + + function mintMulti( + address[] calldata _inputs, + uint256[] calldata _inputQuantities, + uint256 _minOutputQuantity, + address _recipient + ) external virtual returns (uint256 mintOutput); + + function getMintOutput(address _input, uint256 _inputQuantity) + external + view + virtual + returns (uint256 mintOutput); + + function getMintMultiOutput( + address[] calldata _inputs, + uint256[] calldata _inputQuantities + ) external view virtual returns (uint256 mintOutput); + + // Swaps + function swap( + address _input, + address _output, + uint256 _inputQuantity, + uint256 _minOutputQuantity, + address _recipient + ) external virtual returns (uint256 swapOutput); + + function getSwapOutput( + address _input, + address _output, + uint256 _inputQuantity + ) external view virtual returns (uint256 swapOutput); + + // Redemption + function redeem( + address _output, + uint256 _fpTokenQuantity, + uint256 _minOutputQuantity, + address _recipient + ) external virtual returns (uint256 outputQuantity); + + function redeemProportionately( + uint256 _fpTokenQuantity, + uint256[] calldata _minOutputQuantities, + address _recipient + ) external virtual returns (uint256[] memory outputQuantities); + + function redeemExactBassets( + address[] calldata _outputs, + uint256[] calldata _outputQuantities, + uint256 _maxMassetQuantity, + address _recipient + ) external virtual returns (uint256 mAssetRedeemed); + + function getRedeemOutput(address _output, uint256 _fpTokenQuantity) + external + view + virtual + returns (uint256 bAssetOutput); + + function getRedeemExactBassetsOutput( + address[] calldata _outputs, + uint256[] calldata _outputQuantities + ) external view virtual returns (uint256 mAssetAmount); + + // Views + function mAsset() external view virtual returns (address); + + function getPrice() public view virtual returns (uint256 price, uint256 k); +} + +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 + ); +} diff --git a/contracts/mainnet/mstable/main.sol b/contracts/mainnet/mstable/main.sol new file mode 100644 index 00000000..5e938501 --- /dev/null +++ b/contracts/mainnet/mstable/main.sol @@ -0,0 +1,394 @@ +pragma solidity ^0.7.6; + +/** + * @title mStable SAVE. + * @dev Depositing and withdrawing directly to Save + */ + +import { Helpers } from "./helpers.sol"; +import { Events } from "./events.sol"; +import { IMasset, ISavingsContractV2, IBoostedSavingsVault, IFeederPool } from "./interface.sol"; +import { TokenInterface } from "../common/interfaces.sol"; + +abstract contract mStableResolver is Events, Helpers { + // + /*************************************** + CORE + ****************************************/ + + /** + * @dev Deposit to Save via mUSD + * @notice Deposits token supported by mStable to Save + * @param _token Address of token to deposit + * @param _amount Amount of token to deposit + * @return _eventName Event name + * @return _eventParam Event parameters + */ + + function deposit(address _token, uint256 _amount) + external + returns (string memory _eventName, bytes memory _eventParam) + { + return _deposit(_token, _amount, imUsdToken); + } + + /** + * @dev Deposit to Save via bAsset + * @notice Deposits token, requires _minOut for minting + * @param _token Address of token to deposit + * @param _amount Amount of token to deposit + * @param _minOut Minimum amount of token to mint + * @return _eventName Event name + * @return _eventParam Event parameters + */ + + function depositViaMint( + address _token, + uint256 _amount, + uint256 _minOut + ) external returns (string memory _eventName, bytes memory _eventParam) { + // + require( + IMasset(mUsdToken).bAssetIndexes(_token) != 0, + "Token not a bAsset" + ); + + approve(TokenInterface(_token), mUsdToken, _amount); + uint256 mintedAmount = IMasset(mUsdToken).mint( + _token, + _amount, + _minOut, + address(this) + ); + + return _deposit(_token, mintedAmount, mUsdToken); + } + + /** + * @dev Deposit to Save via feeder pool + * @notice Deposits token, requires _minOut for minting and _path + * @param _token Address of token to deposit + * @param _amount Amount of token to deposit + * @param _minOut Minimum amount of token to mint + * @param _path Feeder Pool address for _token + * @return _eventName Event name + * @return _eventParam Event parameters + */ + + function depositViaSwap( + address _token, + uint256 _amount, + uint256 _minOut, + address _path + ) external returns (string memory _eventName, bytes memory _eventParam) { + // + require(_path != address(0), "Path must be set"); + require( + IMasset(mUsdToken).bAssetIndexes(_token) == 0, + "Token is bAsset" + ); + + approve(TokenInterface(_token), _path, _amount); + uint256 mintedAmount = IFeederPool(_path).swap( + _token, + mUsdToken, + _amount, + _minOut, + address(this) + ); + return _deposit(_token, mintedAmount, _path); + } + + /** + * @dev Withdraw from Save to mUSD + * @notice Withdraws from Save Vault to mUSD + * @param _credits Credits to withdraw + * @return _eventName Event name + * @return _eventParam Event parameters + */ + + function withdraw(uint256 _credits) + external + returns (string memory _eventName, bytes memory _eventParam) + { + uint256 amountWithdrawn = _withdraw(_credits); + + _eventName = "LogWithdraw()"; + _eventParam = abi.encode(mUsdToken, amountWithdrawn, imUsdToken); + } + + /** + * @dev Withdraw from Save to bAsset + * @notice Withdraws from Save Vault to bAsset + * @param _token bAsset to withdraw to + * @param _credits Credits to withdraw + * @param _minOut Minimum amount of token to mint + * @return _eventName Event name + * @return _eventParam Event parameters + */ + + function withdrawViaRedeem( + address _token, + uint256 _credits, + uint256 _minOut + ) external returns (string memory _eventName, bytes memory _eventParam) { + // + require( + IMasset(mUsdToken).bAssetIndexes(_token) != 0, + "Token not a bAsset" + ); + + uint256 amountWithdrawn = _withdraw(_credits); + uint256 amountRedeemed = IMasset(mUsdToken).redeem( + _token, + amountWithdrawn, + _minOut, + address(this) + ); + + _eventName = "LogRedeem()"; + _eventParam = abi.encode(mUsdToken, amountRedeemed, _token); + } + + /** + * @dev Withdraw from Save via Feeder Pool + * @notice Withdraws from Save Vault to asset via Feeder Pool + * @param _token bAsset to withdraw to + * @param _credits Credits to withdraw + * @param _minOut Minimum amount of token to mint + * @param _path Feeder Pool address for _token + * @return _eventName Event name + * @return _eventParam Event parameters + */ + + function withdrawViaSwap( + address _token, + uint256 _credits, + uint256 _minOut, + address _path + ) external returns (string memory _eventName, bytes memory _eventParam) { + // + require(_path != address(0), "Path must be set"); + require( + IMasset(mUsdToken).bAssetIndexes(_token) == 0, + "Token is bAsset" + ); + + uint256 amountWithdrawn = _withdraw(_credits); + + approve(TokenInterface(mUsdToken), _path, amountWithdrawn); + uint256 amountRedeemed = IFeederPool(_path).swap( + mUsdToken, + _token, + amountWithdrawn, + _minOut, + address(this) + ); + + _eventName = "LogRedeem()"; + _eventParam = abi.encode(_token, amountRedeemed, _path); + } + + /** + * @dev Claims Rewards + * @notice Claims accrued rewards from the Vault + * @return _eventName Event name + * @return _eventParam Event parameters + */ + + function claimRewards() + external + returns (string memory _eventName, bytes memory _eventParam) + { + address rewardToken = _getRewardTokens(); + uint256 rewardAmount = _getRewardInternalBal(rewardToken); + + IBoostedSavingsVault(imUsdVault).claimReward(); + + uint256 rewardAmountUpdated = _getRewardInternalBal(rewardToken); + + uint256 claimedRewardToken = sub(rewardAmountUpdated, rewardAmount); + + _eventName = "LogClaimRewards()"; + _eventParam = abi.encode(rewardToken, claimedRewardToken); + } + + /** + * @dev Swap tokens + * @notice Swaps tokens via Masset basket + * @param _input Token address to swap from + * @param _output Token address to swap to + * @param _amount Amount of tokens to swap + * @param _minOut Minimum amount of token to mint + * @return _eventName Event name + * @return _eventParam Event parameters + */ + + function swap( + address _input, + address _output, + uint256 _amount, + uint256 _minOut + ) external returns (string memory _eventName, bytes memory _eventParam) { + // + approve(TokenInterface(_input), mUsdToken, _amount); + uint256 amountSwapped; + + // Check the assets and swap accordingly + if (_output == mUsdToken) { + // bAsset to mUSD => mint + amountSwapped = IMasset(mUsdToken).mint( + _input, + _amount, + _minOut, + address(this) + ); + } else if (_input == mUsdToken) { + // mUSD to bAsset => redeem + amountSwapped = IMasset(mUsdToken).redeem( + _output, + _amount, + _minOut, + address(this) + ); + } else { + // bAsset to another bAsset => swap + amountSwapped = IMasset(mUsdToken).swap( + _input, + _output, + _amount, + _minOut, + address(this) + ); + } + + _eventName = "LogSwap()"; + _eventParam = abi.encode(_input, _output, _amount, amountSwapped); + } + + /** + * @dev Swap tokens via Feeder Pool + * @notice Swaps tokens via Feeder Pool + * @param _input Token address to swap from + * @param _output Token address to swap to + * @param _amount Amount of tokens to swap + * @param _minOut Minimum amount of token to mint + * @param _path Feeder Pool address to use + * @return _eventName Event name + * @return _eventParam Event parameters + */ + + function swapViaFeeder( + address _input, + address _output, + uint256 _amount, + uint256 _minOut, + address _path + ) external returns (string memory _eventName, bytes memory _eventParam) { + // + uint256 amountSwapped; + + approve(TokenInterface(_input), _path, _amount); + + // swaps fAsset to mUSD via Feeder Pool + // swaps mUSD to fAsset via Feeder Pool + amountSwapped = IFeederPool(_path).swap( + _input, + _output, + _amount, + _minOut, + address(this) + ); + + _eventName = "LogSwap()"; + _eventParam = abi.encode(_input, _output, _amount, amountSwapped); + } + + /*************************************** + Internal + ****************************************/ + + /** + * @dev Deposit to Save from any asset + * @notice Called internally from deposit functions + * @param _token Address of token to deposit + * @param _amount Amount of token to deposit + * @param _path Path to mint mUSD (only needed for Feeder Pool) + * @return _eventName Event name + * @return _eventParam Event parameters + */ + + function _deposit( + address _token, + uint256 _amount, + address _path + ) internal returns (string memory _eventName, bytes memory _eventParam) { + // + // 1. Deposit mUSD to Save + approve(TokenInterface(mUsdToken), imUsdToken, _amount); + uint256 credits = ISavingsContractV2(imUsdToken).depositSavings( + _amount + ); + + // 2. Stake imUSD to Vault + approve(TokenInterface(imUsdToken), imUsdVault, credits); + IBoostedSavingsVault(imUsdVault).stake(credits); + + // 3. Log Events + _eventName = "LogDeposit()"; + _eventParam = abi.encode(_token, _amount, _path); + } + + /** + * @dev Withdraws from Save + * @notice Withdraws token supported by mStable from Save + * @param _credits Credits to withdraw + * @return amountWithdrawn Amount withdrawn in mUSD + */ + + function _withdraw(uint256 _credits) + internal + returns (uint256 amountWithdrawn) + { + // 1. Withdraw from Vault + // approve(TokenInterface(imUsdVault), imUsdToken, _credits); + IBoostedSavingsVault(imUsdVault).withdraw(_credits); + + // 2. Withdraw from Save + approve(TokenInterface(imUsdToken), imUsdVault, _credits); + amountWithdrawn = ISavingsContractV2(imUsdToken).redeemCredits( + _credits + ); + } + + /** + * @dev Returns the reward tokens + * @notice Gets the reward tokens from the vault contract + * @return rewardToken Address of reward token + */ + + function _getRewardTokens() internal view returns (address rewardToken) { + rewardToken = address( + IBoostedSavingsVault(imUsdVault).getRewardToken() + ); + } + + /** + * @dev Returns the internal balances of the rewardToken and platformToken + * @notice Gets current balances of rewardToken and platformToken, used for calculating rewards accrued + * @param _rewardToken Address of reward token + * @return a Amount of reward token + */ + + function _getRewardInternalBal(address _rewardToken) + internal + view + returns (uint256 a) + { + a = TokenInterface(_rewardToken).balanceOf(address(this)); + } +} + +contract ConnectV2mStable is mStableResolver { + string public constant name = "mStable-Mainnet-Connector-v1"; +}