Last active
January 14, 2022 09:26
-
-
Save davidmitesh/437f7254e9c9c7c8948bbddb75e87545 to your computer and use it in GitHub Desktop.
A coding task for opyn solutions
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// SPDX-License-Identifier: MIT | |
pragma solidity >=0.7.2; | |
pragma experimental ABIEncoderV2; | |
import '@openzeppelin/contracts/access/Ownable.sol'; | |
import '@openzeppelin/contracts/token/ERC20/ERC20.sol'; | |
import '@openzeppelin/contracts/utils/ReentrancyGuard.sol'; | |
import { SafeMath } from '@openzeppelin/contracts/math/SafeMath.sol'; | |
import { IERC20 } from '@openzeppelin/contracts/token/ERC20/IERC20.sol'; | |
import { SafeERC20 } from '@openzeppelin/contracts/token/ERC20/SafeERC20.sol'; | |
// import { IAction } from '../interfaces/IAction.sol'; | |
// import { ICurve } from '../interfaces/ICurve.sol'; | |
// import { IStakeDao } from '../interfaces/IStakeDao.sol'; | |
// -------------****************************************---------------------------------------------- | |
// -------------****************************************---------------------------------------------- | |
// --------------------------EDIT--------------------------------------------------------------------- | |
// -------------****************************************---------------------------------------------- | |
//Upgradeable pool contract | |
import {ILendingPool} from "../interfaces/aave/ILendingPool.sol"; | |
//Proxy contract- immutable and the address will never change | |
import {ILendingPoolAddressesProvider } from "../interfaces/aave/ILendingPoolAddressesProvider.sol" | |
// -------------****************************************---------------------------------------------- | |
// -------------****************************************---------------------------------------------- | |
import "hardhat/console.sol"; | |
/** | |
* Error Codes | |
* O1: actions for the vault have not been initialized | |
* O2: cannot execute transaction, vault is in emergency state | |
* O3: cannot call setActions, actions have already been initialized | |
* O4: action being set is using an invalid address | |
* O5: action being set is a duplicated action | |
* O6: deposited underlying (msg.value) must be greater than 0 | |
* O7: cannot accept underlying deposit, total sdToken controlled by the vault would exceed vault cap | |
* O8: unable to withdraw underlying, sdToken to withdraw would exceed or be equal to the current vault sdToken balance | |
* O9: unable to withdraw underlying, underlying fee transfer to fee recipient (feeRecipient) failed | |
* O10: unable to withdraw underlying, underlying withdrawal to user (msg.sender) failed | |
* O11: cannot close vault positions, vault is not in locked state (VaultState.Locked) | |
* O12: unable to rollover vault, length of allocation percentages (_allocationPercentages) passed is not equal to the initialized actions length | |
* O13: unable to rollover vault, vault is not in unlocked state (VaultState.Unlocked) | |
* O14: unable to rollover vault, the calculated percentage sum (sumPercentage) is greater than the base (BASE) | |
* O15: unable to rollover vault, the calculated percentage sum (sumPercentage) is not equal to the base (BASE) | |
* O16: withdraw reserve percentage must be less than 50% (5000) | |
* O17: cannot call emergencyPause, vault is already in emergency state | |
* O18: cannot call resumeFromPause, vault is not in emergency state | |
* O19: cannot receive underlying from any address other than the curve pool address (curvePool) | |
*/ | |
/** | |
* @title OpynPerpVault | |
* @author Opyn Team | |
* @dev implementation of the Opyn Perp Vault contract that works with stakedao's underlying strategy. | |
* Note that this implementation is meant to only specifically work for the stakedao underlying strategy and is not | |
* a generalized contract. Stakedao's underlying strategy currently accepts curvePool LP tokens called curveLPToken from the | |
* underlying curve pool. This strategy allows users to convert their underlying into yield earning sdToken tokens | |
* and use the sdToken tokens as collateral to sell underlying call options on Opyn. | |
*/ | |
contract OpynPerpVault is ERC20, ReentrancyGuard, Ownable { | |
using SafeERC20 for IERC20; | |
using SafeMath for uint256; | |
enum VaultState { | |
Emergency, | |
Locked, | |
Unlocked | |
} | |
/// @dev actions that build up this strategy (vault) | |
address[] public actions; | |
/// @dev address to which all fees are sent | |
address public feeRecipient; | |
/// @dev address of the underlying address which is earning yields | |
address public underlying; | |
/// @dev stakedao LP token address | |
// address public sdTokenAddress; | |
uint256 public constant BASE = 10000; // 100% | |
/// @dev Cap for the vault. hardcoded at 1000 for initial release | |
uint256 public cap = 1000 ether; | |
/// @dev withdrawal fee percentage. 50 being 0.5% | |
uint256 public withdrawalFeePercentage = 50; | |
/// @dev how many percentage should be reserved in vault for withdraw. 1000 being 10% | |
uint256 public withdrawReserve = 0; | |
/// @dev curvePool for the corresponding stakedao strategy | |
// ICurve public curvePool; | |
/// @dev the curve LP token address for the particular pool | |
// IERC20 public curveLPToken; | |
/// @dev the stakedao strategy contract | |
// IStakeDao stakedaoStrategy; | |
// -------------****************************************---------------------------------------------- | |
// -------------****************************************---------------------------------------------- | |
// --------------------------EDIT--------------------------------------------------------------------- | |
// -------------****************************************---------------------------------------------- | |
//Upgradeable pool contract | |
ILendingPool public lendingPool; | |
//Proxy contract- immutable and the address will never change | |
ILendingPoolAddressesProvider public addressProvider; | |
address public wBtc = [[INSERT_WBTC_ADDRESS_FROM_ETHERSCAN]]; | |
address public aToken = [[INSERT_aWBTC_ADDRESS_FROM_ETHERSCAN]]; // Token we provide liquidity with | |
// @dev do one off approvals here | |
//Making approvals from our contract | |
IERC20Upgradeable(wBtc).safeApprove(LENDING_POOL_ADDRESS, type(uint256).max); | |
IERC20Upgradeable(aToken).safeApprove(LENDING_POOL_ADDRESS, type(uint256).max); | |
// -------------****************************************---------------------------------------------- | |
// -------------****************************************---------------------------------------------- | |
VaultState public state; | |
VaultState public stateBeforePause; | |
/*===================== | |
* Events * | |
*====================*/ | |
event CapUpdated(uint256 newCap); | |
event Deposit(address account, uint256 amountDeposited, uint256 shareMinted); | |
event Rollover(uint256[] allocations); | |
event StateUpdated(VaultState state); | |
event FeeSent(uint256 amount, address feeRecipient); | |
event Withdraw(address account, uint256 amountWithdrawn, uint256 shareBurned); | |
/*===================== | |
* Modifiers * | |
*====================*/ | |
/** | |
* @dev can only be called if actions are initialized | |
*/ | |
function actionsInitialized() private view { | |
require(actions.length > 0, "O1"); | |
} | |
/** | |
* @dev can only be executed if vault is not in emergency state | |
*/ | |
function notEmergency() private view { | |
require(state != VaultState.Emergency, "O2"); | |
} | |
/*===================== | |
* external function * | |
*====================*/ | |
constructor ( | |
address _underlying, | |
// address _sdTokenAddress, | |
// address _curvePoolAddress, | |
address _lendingPoolAddress, | |
address _poolAddressProvider, | |
address _feeRecipient, | |
string memory _tokenName, | |
string memory _tokenSymbol | |
) ERC20(_tokenName, _tokenSymbol) { | |
underlying = _underlying; | |
// sdTokenAddress = _sdTokenAddress; | |
lendingPool = ILendingPool(_lendingPoolAddress); | |
// stakedaoStrategy = IStakeDao(sdTokenAddress); | |
// curveLPToken = stakedaoStrategy.token(); | |
addressProvider = ILendingPoolAddressesProvider(_poolAddressProvider); | |
feeRecipient = _feeRecipient; | |
// curvePool = ICurve(_curvePoolAddress); | |
state = VaultState.Unlocked; | |
} | |
function setActions(address[] memory _actions) external onlyOwner { | |
require(actions.length == 0, "O3"); | |
// assign actions | |
for(uint256 i = 0 ; i < _actions.length; i++ ) { | |
// check all items before actions[i], does not equal to action[i] | |
require(_actions[i] != address(0), "O4"); | |
for(uint256 j = 0; j < i; j++) { | |
require(_actions[i] != _actions[j], "O5"); | |
} | |
actions.push(_actions[i]); | |
} | |
} | |
/** | |
* @notice allows owner to change the cap | |
*/ | |
function setCap(uint256 _newCap) external onlyOwner { | |
cap = _newCap; | |
emit CapUpdated(_newCap); | |
} | |
/** | |
* @notice total sdToken controlled by this vault | |
*/ | |
function totalStakedaoAsset() public view returns (uint256) { | |
uint256 debt = 0; | |
uint256 length = actions.length; | |
for (uint256 i = 0; i < length; i++) { | |
debt = debt.add(IAction(actions[i]).currentValue()); | |
} | |
return _balance().add(debt); | |
} | |
/** | |
* total underlying value of the sdToken controlled by this vault | |
*/ | |
function totalUnderlyingControlled() external view returns (uint256) { | |
// hard coded to 36 because crv LP token and sdToken are both 18 decimals. | |
return totalStakedaoAsset().mul(stakedaoStrategy.getPricePerFullShare()).mul(curvePool.get_virtual_price()).div(10**36); | |
} | |
/** | |
* @dev return how many sdToken you can get if you burn the number of shares, after charging the fee. | |
*/ | |
function getWithdrawAmountByShares(uint256 _shares) external view returns (uint256) { | |
uint256 withdrawAmount = _getWithdrawAmountByShares(_shares); | |
return withdrawAmount.sub(_getWithdrawFee(withdrawAmount)); | |
} | |
/** | |
* @notice Deposits underlying into the contract and mint vault shares. | |
* @dev deposit into the curvePool, then into stakedao, then mint the shares to depositor, and emit the deposit event | |
* @param amount amount of underlying to deposit | |
* @param minCrvLPToken minimum amount of curveLPToken to get out from adding liquidity. | |
*/ | |
// -------------****************************************---------------------------------------------- | |
// -------------****************************************---------------------------------------------- | |
// --------------------------EDIT--------------------------------------------------------------------- | |
// -------------****************************************---------------------------------------------- | |
function depositWBTC(uint256 amount) external nonReentrant { | |
notEmergency(); | |
actionsInitialized() | |
require(amount > 0, 'O6'); | |
//getting the proxy pool address for the contrac at the current time | |
address poolAddress = addressProvider.getLendingPool(); | |
//At this point, the required amount of wbtc is already approved and deposited in this contract by the client code. | |
uint256 totalaTokenBalanceBeforeDeposit = aToken.balanceOf(address(this)); | |
lendingPool.deposit(wBtc, amount, address(this), 0);//Here aWBTC tokens is transferred to this contract | |
// underlyingToken.safeTransferFrom(msg.sender, address(this), amount); | |
uint256 totalaTokenWithDepositedAmount = aToken.balanceOf(address(this)); | |
require(totalaTokenWithDepositedAmount < cap, 'O7'); | |
uint256 aTokenDeposited = totalaTokenWithDepositedAmount.sub(totalaTokenBalanceBeforeDeposit); | |
uint256 share = _getSharesByDepositAmount(aTokenDeposited, totalaTokenBalanceBeforeDeposit); | |
emit Deposit(msg.sender, msg.value, share); | |
_mint(msg.sender, share); | |
} | |
// -------------****************************************---------------------------------------------- | |
// -------------****************************************---------------------------------------------- | |
function depositUnderlying(uint256 amount, uint256 minCrvLPToken) external nonReentrant { | |
notEmergency(); | |
actionsInitialized(); | |
require(amount > 0, 'O6'); | |
// the sdToken is already deposited into the contract at this point, need to substract it from total | |
uint256[3] memory amounts; | |
amounts[0] = 0; // not depositing any rebBTC | |
amounts[1] = amount; | |
amounts[2] = 0; | |
// deposit underlying to curvePool | |
IERC20 underlyingToken = IERC20(underlying); | |
underlyingToken.safeTransferFrom(msg.sender, address(this), amount); | |
underlyingToken.approve(address(curvePool), amount); | |
curvePool.add_liquidity(amounts, minCrvLPToken); | |
_depositToStakedaoAndMint(); | |
} | |
/** | |
* @notice Deposits curve LP into the contract and mint vault shares. | |
* @dev deposit into stakedao, then mint the shares to depositor, and emit the deposit event | |
* @param amount amount of curveLP to deposit | |
*/ | |
function depositCrvLP(uint256 amount) external nonReentrant { | |
notEmergency(); | |
actionsInitialized(); | |
require(amount > 0, 'O6'); | |
// deposit underlying to curvePool | |
curveLPToken.safeTransferFrom(msg.sender, address(this), amount); | |
_depositToStakedaoAndMint(); | |
} | |
/** | |
* @notice Withdraws underlying from vault using vault shares | |
* @dev burns shares, withdraws curveLPToken from stakdao, withdraws underlying from curvePool | |
* @param _share is the number of vault shares to be burned | |
*/ | |
function withdrawUnderlying(uint256 _share, uint256 _minUnderlying) external nonReentrant { | |
// withdraw from curve | |
IERC20 underlyingToken = IERC20(underlying); | |
uint256 underlyingBalanceBefore = underlyingToken.balanceOf(address(this)); | |
uint256 curveLPTokenBalance = _withdrawFromStakedao(_share); | |
curvePool.remove_liquidity_one_coin(curveLPTokenBalance, 1, _minUnderlying); | |
uint256 underlyingBalanceAfter = underlyingToken.balanceOf(address(this)); | |
uint256 underlyingOwedToUser = underlyingBalanceAfter.sub(underlyingBalanceBefore); | |
// send underlying to user | |
underlyingToken.safeTransfer(msg.sender, underlyingOwedToUser); | |
emit Withdraw(msg.sender, underlyingOwedToUser, _share); | |
} | |
/** | |
* @notice Withdraws curveLPToken from stakedao | |
* @dev burns shares, withdraws curveLPToken from stakdao | |
* @param _share is the number of vault shares to be burned | |
*/ | |
function withdrawCrvLp (uint256 _share) external nonReentrant { | |
uint256 curveLPTokenBalance = _withdrawFromStakedao(_share); | |
curveLPToken.safeTransfer(msg.sender, curveLPTokenBalance); | |
} | |
/** | |
* @notice anyone can call this to close out the previous round by calling "closePositions" on all actions. | |
* @dev iterrate through each action, close position and withdraw funds | |
*/ | |
function closePositions() public { | |
actionsInitialized(); | |
require(state == VaultState.Locked, "O11"); | |
state = VaultState.Unlocked; | |
address cacheAddress = sdTokenAddress; | |
address[] memory cacheActions = actions; | |
for (uint256 i = 0; i < cacheActions.length; i = i + 1) { | |
// 1. close position. this should revert if any position is not ready to be closed. | |
IAction(cacheActions[i]).closePosition(); | |
// 2. withdraw sdTokens from the action | |
uint256 actionBalance = IERC20(cacheAddress).balanceOf(cacheActions[i]); | |
if (actionBalance > 0) | |
IERC20(cacheAddress).safeTransferFrom(cacheActions[i], address(this), actionBalance); | |
} | |
emit StateUpdated(VaultState.Unlocked); | |
} | |
/** | |
* @notice can only be called when the vault is unlocked. It sets the state to locked and distributes funds to each action. | |
*/ | |
function rollOver(uint256[] calldata _allocationPercentages) external onlyOwner nonReentrant { | |
actionsInitialized(); | |
require(_allocationPercentages.length == actions.length, 'O12'); | |
require(state == VaultState.Unlocked, "O13"); | |
state = VaultState.Locked; | |
address cacheAddress = sdTokenAddress; | |
address[] memory cacheActions = actions; | |
uint256 cacheBase = BASE; | |
uint256 cacheTotalAsset = totalStakedaoAsset(); | |
// keep track of total percentage to make sure we're summing up to 100% | |
uint256 sumPercentage = withdrawReserve; | |
for (uint256 i = 0; i < _allocationPercentages.length; i = i + 1) { | |
sumPercentage = sumPercentage.add(_allocationPercentages[i]); | |
require(sumPercentage <= cacheBase, 'O14'); | |
uint256 newAmount = cacheTotalAsset.mul(_allocationPercentages[i]).div(cacheBase); | |
if (newAmount > 0) IERC20(cacheAddress).safeTransfer(cacheActions[i], newAmount); | |
IAction(cacheActions[i]).rolloverPosition(); | |
} | |
require(sumPercentage == cacheBase, 'O15'); | |
emit Rollover(_allocationPercentages); | |
emit StateUpdated(VaultState.Locked); | |
} | |
/** | |
* @dev set the vault withdrawal fee recipient | |
*/ | |
function setWithdrawalFeeRecipient(address _newWithdrawalFeeRecipient) external onlyOwner { | |
feeRecipient = _newWithdrawalFeeRecipient; | |
} | |
/** | |
* @dev set the percentage that should be reserved in vault for withdraw | |
*/ | |
function setWithdrawalFeePercentage(uint256 _newWithdrawalFeePercentage) external onlyOwner { | |
withdrawalFeePercentage = _newWithdrawalFeePercentage; | |
} | |
/** | |
* @dev set the percentage that should be reserved in vault for withdraw | |
*/ | |
function setWithdrawReserve(uint256 _reserve) external onlyOwner { | |
require(_reserve < 5000, "O16"); | |
withdrawReserve = _reserve; | |
} | |
/** | |
* @dev set the state to "Emergency", which disable all withdraw and deposit | |
*/ | |
function emergencyPause() external onlyOwner { | |
require(state != VaultState.Emergency, "O17"); | |
stateBeforePause = state; | |
state = VaultState.Emergency; | |
emit StateUpdated(VaultState.Emergency); | |
} | |
/** | |
* @dev set the state from "Emergency", which disable all withdraw and deposit | |
*/ | |
function resumeFromPause() external onlyOwner { | |
require(state == VaultState.Emergency, "O18"); | |
state = stateBeforePause; | |
emit StateUpdated(stateBeforePause); | |
} | |
/** | |
* @dev return how many shares you can get if you deposit {_amount} sdToken | |
* @param _amount amount of token depositing | |
*/ | |
function getSharesByDepositAmount(uint256 _amount) external view returns (uint256) { | |
return _getSharesByDepositAmount(_amount, totalStakedaoAsset()); | |
} | |
/*===================== | |
* Internal functions * | |
*====================*/ | |
function _depositToStakedaoAndMint() internal { | |
// keep track of balance before | |
uint256 totalSdTokenBalanceBeforeDeposit = totalStakedaoAsset(); | |
// deposit curveLPToken to stakedao | |
uint256 curveLPTokenToDeposit = curveLPToken.balanceOf(address(this)); | |
curveLPToken.safeIncreaseAllowance(sdTokenAddress, curveLPTokenToDeposit); | |
stakedaoStrategy.deposit(curveLPTokenToDeposit); | |
// mint shares and emit event | |
uint256 totalSdTokenWithDepositedAmount = totalStakedaoAsset(); | |
require(totalSdTokenWithDepositedAmount < cap, 'O7'); | |
uint256 sdTokenDeposited = totalSdTokenWithDepositedAmount.sub(totalSdTokenBalanceBeforeDeposit); | |
uint256 share = _getSharesByDepositAmount(sdTokenDeposited, totalSdTokenBalanceBeforeDeposit); | |
emit Deposit(msg.sender, msg.value, share); | |
_mint(msg.sender, share); | |
} | |
function _withdrawFromStakedao(uint256 _share) internal returns (uint256) { | |
notEmergency(); | |
actionsInitialized(); | |
uint256 currentSdTokenBalance = _balance(); | |
uint256 sdTokenToShareOfRecipient = _getWithdrawAmountByShares(_share); | |
uint256 fee = _getWithdrawFee(sdTokenToShareOfRecipient); | |
uint256 sdTokenToWithdraw = sdTokenToShareOfRecipient.sub(fee); | |
require(sdTokenToWithdraw <= currentSdTokenBalance, 'O8'); | |
// burn shares | |
_burn(msg.sender, _share); | |
// withdraw from stakedao | |
stakedaoStrategy.withdraw(sdTokenToWithdraw); | |
// transfer fee to recipient | |
IERC20 stakedaoToken = IERC20(sdTokenAddress); | |
stakedaoToken.safeTransfer(feeRecipient, fee); | |
emit FeeSent(fee, feeRecipient); | |
return curveLPToken.balanceOf(address(this)); | |
} | |
/** | |
* @dev returns remaining sdToken balance in the vault. | |
*/ | |
function _balance() internal view returns (uint256) { | |
return IERC20(sdTokenAddress).balanceOf(address(this)); | |
} | |
/** | |
* @dev return how many shares you can get if you deposit {_amount} sdToken | |
* @param _amount amount of token depositing | |
* @param _totalAssetAmount amont of sdToken already in the pool before deposit | |
*/ | |
function _getSharesByDepositAmount(uint256 _amount, uint256 _totalAssetAmount) internal view returns (uint256) { | |
uint256 shareSupply = totalSupply(); | |
// share amount | |
return shareSupply == 0 ? _amount : _amount.mul(shareSupply).div(_totalAssetAmount); | |
} | |
/** | |
* @dev return how many sdToken you can get if you burn the number of shares | |
*/ | |
function _getWithdrawAmountByShares(uint256 _share) internal view returns (uint256) { | |
// withdrawal amount | |
return _share.mul(totalStakedaoAsset()).div(totalSupply()); | |
} | |
/** | |
* @dev get amount of fee charged based on total amount of wunderlying withdrawing. | |
*/ | |
function _getWithdrawFee(uint256 _withdrawAmount) internal view returns (uint256) { | |
return _withdrawAmount.mul(withdrawalFeePercentage).div(BASE); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment