Skip to content

Commit

Permalink
Flexible SaveWrapper
Browse files Browse the repository at this point in the history
- Make SaveWrapper stateless (that is, aside from being `Ownable`) so it can be flexible about assets supported
- Define pattern for discrete Hardhat tasks with params
- Disable some eslint rules
  • Loading branch information
JamesLefrere committed Apr 3, 2021
1 parent fed3f2a commit 2c02d72
Show file tree
Hide file tree
Showing 9 changed files with 370 additions and 93 deletions.
4 changes: 3 additions & 1 deletion .eslintrc.js
Expand Up @@ -26,7 +26,9 @@ module.exports = {
},
"rules": {
"@typescript-eslint/no-use-before-define": 1,
"import/no-extraneous-dependencies": 1,
"import/no-extraneous-dependencies": "off",
"no-console": "off",
"import/prefer-default-export": "off",
"no-nested-ternary": 1,
"@typescript-eslint/dot-notation": 1
},
Expand Down
235 changes: 158 additions & 77 deletions contracts/savings/peripheral/SaveWrapper.sol
Expand Up @@ -6,10 +6,11 @@ import { IMasset } from "../../interfaces/IMasset.sol";

import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol";

import { IUniswapV2Router02 } from "../../interfaces/IUniswapV2Router02.sol";
import { IBasicToken } from "../../shared/IBasicToken.sol";


interface IBoostedSavingsVault {
function stake(address _beneficiary, uint256 _amount) external;
}
Expand All @@ -18,125 +19,205 @@ interface IBoostedSavingsVault {
// 0 - SAVE
// 1 - MINT AND SAVE
// 2 - BUY AND SAVE (ETH via Uni)
contract SaveWrapper {

contract SaveWrapper is Ownable {
using SafeERC20 for IERC20;

// Constants - add to bytecode during deployment
address public immutable save;
address public immutable vault;
address public immutable mAsset;

IUniswapV2Router02 public immutable uniswap;

constructor(
/**
* @dev 0. Simply saves an mAsset and then into the vault
* @param _mAsset mAsset address
* @param _save Save address
* @param _vault Boosted Savings Vault address
* @param _amount Units of mAsset to deposit to savings
*/
function saveAndStake(
address _mAsset,
address _save,
address _vault,
address _mAsset,
address[] memory _bAssets,
address _uniswapAddress
) {
require(_save != address(0), "Invalid save address");
save = _save;
require(_vault != address(0), "Invalid vault address");
vault = _vault;
require(_mAsset != address(0), "Invalid mAsset address");
mAsset = _mAsset;
require(_uniswapAddress != address(0), "Invalid uniswap address");
uniswap = IUniswapV2Router02(_uniswapAddress);

IERC20(_mAsset).safeApprove(_save, 2**256 - 1);
IERC20(_save).approve(_vault, 2**256 - 1);
for(uint256 i = 0; i < _bAssets.length; i++ ) {
IERC20(_bAssets[i]).safeApprove(_mAsset, 2**256 - 1);
}
}
uint256 _amount
) external {
require(_mAsset != address(0), "Invalid mAsset");
require(_save != address(0), "Invalid save");
require(_vault != address(0), "Invalid vault");

// 1. Get the input mAsset
IERC20(_mAsset).transferFrom(msg.sender, address(this), _amount);

/**
* @dev 0. Simply saves an mAsset and then into the vault
* @param _amount Units of mAsset to deposit to savings
*/
function saveAndStake(uint256 _amount) external {
IERC20(mAsset).transferFrom(msg.sender, address(this), _amount);
_saveAndStake(_amount, true);
// 2. Mint imAsset and stake in vault
_saveAndStake(_save, _vault, _amount, true);
}

/**
* @dev 1. Mints an mAsset and then deposits to SAVE
* @param _mAsset mAsset address
* @param _bAsset bAsset address
* @param _amt Amount of bAsset to mint with
* @param _save Save address
* @param _vault Boosted Savings Vault address
* @param _amount Amount of bAsset to mint with
* @param _minOut Min amount of mAsset to get back
* @param _stake Add the imUSD to the Savings Vault?
* @param _stake Add the imAsset to the Boosted Savings Vault?
*/
function saveViaMint(address _bAsset, uint256 _amt, uint256 _minOut, bool _stake) external {
function saveViaMint(
address _mAsset,
address _save,
address _vault,
address _bAsset,
uint256 _amount,
uint256 _minOut,
bool _stake
) external {
require(_mAsset != address(0), "Invalid mAsset");
require(_save != address(0), "Invalid save");
require(_vault != address(0), "Invalid vault");
require(_bAsset != address(0), "Invalid bAsset");

// 1. Get the input bAsset
IERC20(_bAsset).transferFrom(msg.sender, address(this), _amt);
IERC20(_bAsset).transferFrom(msg.sender, address(this), _amount);

// 2. Mint
IMasset mAsset_ = IMasset(mAsset);
uint256 massetsMinted = mAsset_.mint(_bAsset, _amt, _minOut, address(this));
// 3. Mint imUSD and optionally stake in vault
_saveAndStake(massetsMinted, _stake);
}
uint256 massetsMinted = IMasset(_mAsset).mint(_bAsset, _amount, _minOut, address(this));

// 3. Mint imAsset and optionally stake in vault
_saveAndStake(_save, _vault, massetsMinted, _stake);
}

/**
* @dev 2. Buys a bAsset on Uniswap with ETH then mUSD on Curve
* @param _amountOutMin Min uniswap output in bAsset units
* @param _path Sell path on Uniswap (e.g. [WETH, DAI])
* @param _minOutMStable Min amount of mUSD to receive
* @param _stake Add the imUSD to the Savings Vault?
* @dev 2. Buys a bAsset on Uniswap with ETH, then mints imAsset via mAsset,
* optionally staking in the Boosted Savings Vault
* @param _mAsset mAsset address
* @param _save Save address
* @param _vault Boosted vault address
* @param _uniswap Uniswap router address
* @param _amountOutMin Min uniswap output in bAsset units
* @param _path Sell path on Uniswap (e.g. [WETH, DAI])
* @param _minOutMStable Min amount of mAsset to receive
* @param _stake Add the imAsset to the Savings Vault?
*/
function saveViaUniswapETH(
address _mAsset,
address _save,
address _vault,
address _uniswap,
uint256 _amountOutMin,
address[] calldata _path,
uint256 _minOutMStable,
bool _stake
) external payable {
require(_mAsset != address(0), "Invalid mAsset");
require(_save != address(0), "Invalid save");
require(_vault != address(0), "Invalid vault");
require(_uniswap != address(0), "Invalid uniswap");

// 1. Get the bAsset
uint[] memory amounts = uniswap.swapExactETHForTokens{value: msg.value}(
_amountOutMin,
_path,
address(this),
block.timestamp + 1000
);
// 2. Purchase mUSD
uint256 massetsMinted = IMasset(mAsset).mint(_path[_path.length-1], amounts[amounts.length-1], _minOutMStable, address(this));
// 3. Mint imUSD and optionally stake in vault
_saveAndStake(massetsMinted, _stake);
uint256[] memory amounts =
IUniswapV2Router02(_uniswap).swapExactETHForTokens{ value: msg.value }(
_amountOutMin,
_path,
address(this),
block.timestamp + 1000
);

// 2. Purchase mAsset
uint256 massetsMinted =
IMasset(_mAsset).mint(
_path[_path.length - 1],
amounts[amounts.length - 1],
_minOutMStable,
address(this)
);

// 3. Mint imAsset and optionally stake in vault
_saveAndStake(_save, _vault, massetsMinted, _stake);
}

/**
* @dev Gets estimated mAsset output from a WETH > bAsset > mAsset trade
* @param _mAsset mAsset address
* @param _uniswap Uniswap router address
* @param _ethAmount ETH amount to sell
* @param _path Sell path on Uniswap (e.g. [WETH, DAI])
*/
function estimate_saveViaUniswapETH(
address _mAsset,
address _uniswap,
uint256 _ethAmount,
address[] calldata _path
)
external
view
returns (uint256 out)
{
uint256 estimatedBasset = _getAmountOut(_ethAmount, _path);
return IMasset(mAsset).getMintOutput(_path[_path.length-1], estimatedBasset);
) external view returns (uint256 out) {
require(_mAsset != address(0), "Invalid mAsset");
require(_uniswap != address(0), "Invalid uniswap");

uint256 estimatedBasset = _getAmountOut(_uniswap, _ethAmount, _path);
return IMasset(_mAsset).getMintOutput(_path[_path.length - 1], estimatedBasset);
}

/** @dev Internal func to deposit into SAVE and optionally stake in the vault */
/** @dev Internal func to deposit into Save and optionally stake in the vault
* @param _save Save address
* @param _vault Boosted vault address
* @param _amount Amount of mAsset to deposit
* @param _stake Add the imAsset to the Savings Vault?
*/
function _saveAndStake(
address _save,
address _vault,
uint256 _amount,
bool _stake
) internal {
if(_stake){
uint256 credits = ISavingsContractV2(save).depositSavings(_amount, address(this));
IBoostedSavingsVault(vault).stake(msg.sender, credits);
if (_stake) {
uint256 credits = ISavingsContractV2(_save).depositSavings(_amount, address(this));
IBoostedSavingsVault(_vault).stake(msg.sender, credits);
} else {
ISavingsContractV2(save).depositSavings(_amount, msg.sender);
ISavingsContractV2(_save).depositSavings(_amount, msg.sender);
}
}

/** @dev Internal func to get esimtated Uniswap output from WETH to token trade */
function _getAmountOut(uint256 _amountIn, address[] memory _path) internal view returns (uint256) {
uint256[] memory amountsOut = uniswap.getAmountsOut(_amountIn, _path);
/** @dev Internal func to get estimated Uniswap output from WETH to token trade */
function _getAmountOut(
address _uniswap,
uint256 _amountIn,
address[] memory _path
) internal view returns (uint256) {
uint256[] memory amountsOut = IUniswapV2Router02(_uniswap).getAmountsOut(_amountIn, _path);
return amountsOut[amountsOut.length - 1];
}
}

/**
* @dev Approve mAsset, Save and multiple bAssets
*/
function approve(
address _mAsset,
address _save,
address _vault,
address[] calldata _bAssets
) external onlyOwner {
_approve(_mAsset, _save);
_approve(_save, _vault);
_approve(_bAssets, _mAsset);
}

/**
* @dev Approve one token/spender
*/
function approve(address _token, address _spender) external onlyOwner {
_approve(_token, _spender);
}

/**
* @dev Approve multiple tokens/one spender
*/
function approve(address[] calldata _tokens, address _spender) external onlyOwner {
_approve(_tokens, _spender);
}

function _approve(address _token, address _spender) internal {
require(_spender != address(0), "Invalid spender");
require(_token != address(0), "Invalid token");
IERC20(_token).safeApprove(_spender, 2**256 - 1);
}

function _approve(address[] calldata _tokens, address _spender) internal {
require(_spender != address(0), "Invalid spender");
for (uint256 i = 0; i < _tokens.length; i++) {
require(_tokens[i] != address(0), "Invalid token");
IERC20(_tokens[i]).safeApprove(_spender, 2**256 - 1);
}
}
}
1 change: 1 addition & 0 deletions package.json
Expand Up @@ -46,6 +46,7 @@
"@typescript-eslint/eslint-plugin-tslint": "^4.14.0",
"@typescript-eslint/parser": "^4.14.0",
"chai": "^4.2.0",
"chalk": "^4.1.0",
"csv-parse": "^4.15.0",
"eslint": "^7.18.0",
"eslint-config-airbnb-typescript": "^12.0.0",
Expand Down
1 change: 1 addition & 0 deletions tasks.config.ts
Expand Up @@ -5,5 +5,6 @@ import "./tasks/mBTC"
import "./tasks/deployFeeders"
import "./tasks/deployMV3"
import "./tasks/mUSD"
import "./tasks/SaveWrapper"

export default config
73 changes: 73 additions & 0 deletions tasks/SaveWrapper.ts
@@ -0,0 +1,73 @@
import "ts-node/register"
import "tsconfig-paths/register"
import { task } from "hardhat/config"

import { params, deployTx, sendTx } from "./taskUtils"
import { SaveWrapper__factory } from "../types/generated"

task("SaveWrapper.deploy", "Deploy a new SaveWrapper").setAction(async (taskArgs, { ethers }) => {
const [deployer] = await ethers.getSigners()
await deployTx(deployer, SaveWrapper__factory, "SaveWrapper")
})

task("SaveWrapper.approveMasset", "Sets approvals for a new mAsset")
.addParam("saveWrapper", "SaveWrapper address", undefined, params.address, false)
.addParam("masset", "mAsset address", undefined, params.address, false)
.addParam("bassets", "bAsset addresses", undefined, params.addressArray, false)
.addParam("save", "Save contract address (i.e. imAsset)", undefined, params.address, false)
.addParam("vault", "BoostedSavingsVault contract address", undefined, params.address, false)
.setAction(
async (
{
saveWrapper,
masset,
vault,
bassets,
save,
}: { saveWrapper: string; masset: string; bassets: string[]; save: string; vault: string },
{ ethers },
) => {
const [deployer] = await ethers.getSigners()
await sendTx(
SaveWrapper__factory.connect(saveWrapper, deployer),
"approve(address,address,address,address[])",
"Approve mAsset and other assets",
masset,
save,
vault,
bassets,
)
},
)

task("SaveWrapper.approveMulti", "Sets approvals for multiple tokens/a single spender")
.addParam("saveWrapper", "SaveWrapper address", undefined, params.address, false)
.addParam("tokens", "Token addresses", undefined, params.address, false)
.addParam("spender", "Spender address", undefined, params.address, false)
.setAction(async ({ saveWrapper, tokens, spender }: { saveWrapper: string; tokens: string[]; spender: string }, { ethers }) => {
const [deployer] = await ethers.getSigners()
await sendTx(
SaveWrapper__factory.connect(saveWrapper, deployer),
"approve(address[],address)",
"Approve muliple tokens/single spender",
tokens,
spender,
)
})

task("SaveWrapper.approve", "Sets approvals for a single token/spender")
.addParam("saveWrapper", "SaveWrapper address", undefined, params.address, false)
.addParam("token", "Token address", undefined, params.address, false)
.addParam("spender", "Spender address", undefined, params.address, false)
.setAction(async ({ saveWrapper, token, spender }: { saveWrapper: string; token: string; spender: string }, { ethers }) => {
const [deployer] = await ethers.getSigners()
await sendTx(
SaveWrapper__factory.connect(saveWrapper, deployer),
"approve(address,address)",
"Approve single token/spender",
token,
spender,
)
})

export {}

0 comments on commit 2c02d72

Please sign in to comment.