diff --git a/contracts/core/BorrowableMarginPool.sol b/contracts/core/BorrowableMarginPool.sol new file mode 100644 index 000000000..bbcb5cdc7 --- /dev/null +++ b/contracts/core/BorrowableMarginPool.sol @@ -0,0 +1,354 @@ +/** + * SPDX-License-Identifier: UNLICENSED + */ +pragma solidity =0.6.10; + +import {ERC20Interface} from "../interfaces/ERC20Interface.sol"; +import {OtokenInterface} from "../interfaces/OtokenInterface.sol"; +import {AddressBookInterface} from "../interfaces/AddressBookInterface.sol"; +import {WhitelistInterface} from "../interfaces/WhitelistInterface.sol"; +import {SafeMath} from "../packages/oz/SafeMath.sol"; +import {SafeERC20} from "../packages/oz/SafeERC20.sol"; +import {MarginPool} from "./MarginPool.sol"; + +/** + * @author Ribbon Team + * @title MarginPoolV2 + * @notice Contract that holds all protocol funds AND allows collateral borrows + */ +contract BorrowableMarginPool is MarginPool { + using SafeMath for uint256; + using SafeERC20 for ERC20Interface; + + uint16 public constant TOTAL_PCT = 10000; // Equals 100% + + /// @dev mapping between collateral asset and borrow PCT + mapping(address => uint256) public borrowPCT; + /// @dev mapping between address and whitelist status of borrower + mapping(address => bool) internal whitelistedBorrower; + /// @dev mapping between address and whitelist status of otoken buyer + /// This is the whitelist for all the holders of oTokens that have a claim to + /// collateral in this pool + mapping(address => bool) internal whitelistedOTokenBuyer; + /// @dev mapping between address and whitelist status of options vault + /// This denotes whether an options vault can create a margin vault with + /// collateral custodied in this borrowable pool + mapping(address => bool) internal whitelistedOptionsVault; + /// @dev mapping between address and whether vault is a retail vault + mapping(address => bool) internal retailVaultStatus; + /// @dev mapping between (borrower, asset) and outstanding borrower amount + mapping(address => mapping(address => uint256)) public borrowed; + + /** + * @notice contructor + * @param _addressBook AddressBook module + */ + constructor(address _addressBook) public MarginPool(_addressBook) {} + + /// @notice emit event when a borrower is whitelisted / blacklisted + event SetBorrowWhitelist(address indexed borrower, bool whitelisted); + /// @notice emit event when a oToken buyer is whitelisted / blacklisted + event SetOTokenBuyerWhitelist(address indexed oTokenBuyer, bool whitelisted); + /// @notice emit event when a options vault is whitelisted / blacklisted + event SetOptionsVaultWhitelist(address indexed optionsVault, bool whitelisted); + /// @notice emit event when a options vault is set to retail status + event SetOptionsVaultToRetailStatus(address indexed optionsVault); + /// @notice emit event when borrowing percent has been changed + event SetBorrowPCT(address indexed collateralAsset, uint256 borrowPCT); + /// @notice emit event when a borrower borrows an asset + event Borrow(address indexed oToken, address indexed collateralAsset, uint256 amount, address indexed borrower); + /// @notice emit event when a loan is repaid + event Repay( + address indexed oToken, + address indexed collateralAsset, + uint256 amount, + address indexed borrower, + address repayer + ); + + /** + * @notice check if the sender is whitelisted + */ + modifier onlyWhitelistedBorrower() { + require(whitelistedBorrower[msg.sender], "MarginPool: Sender is not whitelisted borrower"); + + _; + } + + /** + * @notice check if a borrower is whitelisted + * @param _borrower address of the borrower + * @return boolean, True if the borrower is whitelisted + */ + function isWhitelistedBorrower(address _borrower) external view returns (bool) { + return whitelistedBorrower[_borrower]; + } + + /** + * @notice check if a oToken buyer is whitelisted + * @param _oTokenBuyer address of the oToken buyer + * @return boolean, True if the oToken buyer is whitelisted + */ + function isWhitelistedOTokenBuyer(address _oTokenBuyer) external view returns (bool) { + return whitelistedOTokenBuyer[_oTokenBuyer]; + } + + /** + * @notice check if a options vault is whitelisted + * @param _optionsVault address of the options vault + * @return boolean, True if the options vault is whitelisted + */ + function isWhitelistedOptionsVault(address _optionsVault) external view returns (bool) { + return whitelistedOptionsVault[_optionsVault]; + } + + /** + * @notice check if a options vault is retail vault + * @param _optionsVault address of the options vault + * @return boolean, True if the options vault is a retail vault + */ + function isRetailOptionsVault(address _optionsVault) external view returns (bool) { + return retailVaultStatus[_optionsVault]; + } + + /** + * @notice Set borrower whitelist status + * @param _borrower address of the borrower (100% of the time verified market makers) + * @param _whitelisted bool of whether it is whitelisted or blacklisted + */ + function setBorrowerWhitelistedStatus(address _borrower, bool _whitelisted) external onlyOwner { + require(_borrower != address(0), "MarginPool: Invalid Borrower"); + + whitelistedBorrower[_borrower] = _whitelisted; + emit SetBorrowWhitelist(_borrower, _whitelisted); + } + + /** + * @notice Set oToken buyer whitelist status + * @param _oTokenBuyer address of the oToken buyer + * @param _whitelisted bool of whether it is whitelisted or blacklisted + */ + function setOTokenBuyerWhitelistedStatus(address _oTokenBuyer, bool _whitelisted) external onlyOwner { + require(_oTokenBuyer != address(0), "MarginPool: Invalid oToken Buyer"); + + whitelistedOTokenBuyer[_oTokenBuyer] = _whitelisted; + emit SetOTokenBuyerWhitelist(_oTokenBuyer, _whitelisted); + } + + /** + * @notice Set options vault whitelist status + * @param _optionsVault address of the options vault + * @param _whitelisted bool of whether it is whitelisted or blacklisted + */ + function setOptionsVaultWhitelistedStatus(address _optionsVault, bool _whitelisted) external onlyOwner { + require(_optionsVault != address(0), "MarginPool: Invalid Options Vault"); + // Prevent whitelist of retail vault + require(!retailVaultStatus[_optionsVault], "MarginPool: Cannot whitelist a retail vault"); + + whitelistedOptionsVault[_optionsVault] = _whitelisted; + emit SetOptionsVaultWhitelist(_optionsVault, _whitelisted); + } + + /** + * @notice Set whether vault is a retail vault + * @param _optionsVaults address of the options vault + */ + function setOptionsVaultToRetailStatus(address[] calldata _optionsVaults) external onlyOwner { + for (uint256 i = 0; i < _optionsVaults.length; i++) { + if (_optionsVaults[i] == address(0) || retailVaultStatus[_optionsVaults[i]]) { + continue; + } + + // Prevents setting to false to avoid multisig risk of + // redirecting retail funds to borrowable margin pool + retailVaultStatus[_optionsVaults[i]] = true; + emit SetOptionsVaultToRetailStatus(_optionsVaults[i]); + } + } + + /** + * @notice Set Borrow percent of collateral asset + * @param _collateral address of collateral asset + * @param _borrowPCT borrow PCT + */ + function setBorrowPCT(address _collateral, uint256 _borrowPCT) external onlyOwner { + borrowPCT[_collateral] = _borrowPCT; + emit SetBorrowPCT(_collateral, _borrowPCT); + } + + /** + * @notice Lends out asset to market maker + * @param _oToken address of the oToken laying claim to the collateral assets + * @param _oTokenAmount amount of the oToken to post as collateral in exchange + */ + function borrow(address _oToken, uint256 _oTokenAmount) public onlyWhitelistedBorrower { + require( + WhitelistInterface(AddressBookInterface(addressBook).getWhitelist()).isWhitelistedOtoken(_oToken), + "MarginPool: oToken is not whitelisted" + ); + + require(_oTokenAmount > 0, "MarginPool: Cannot borrow 0 of underlying"); + + OtokenInterface oToken = OtokenInterface(_oToken); + + address collateralAsset = oToken.collateralAsset(); + uint256 outstandingAssetBorrow = borrowed[msg.sender][collateralAsset]; + + require(!oToken.isPut(), "MarginPool: oToken is not a call option"); + + // Make sure borrow does not attempt to borrow with an expired oToken + require( + oToken.expiryTimestamp() > block.timestamp, + "MarginPool: Cannot borrow collateral asset of expired oToken" + ); + + uint256 oTokenBalance = ERC20Interface(_oToken).balanceOf(msg.sender); + + // Each oToken represents 1:1 of collateral token + // So a market maker can borrow at most our oToken balance in the collateral asset + uint256 oTokenAmountCustodied = _collateralAssetToOTokenAmount(collateralAsset, outstandingAssetBorrow); + uint256 totalBorrowable = oTokenBalance.add(oTokenAmountCustodied).mul(borrowPCT[collateralAsset]).div( + TOTAL_PCT + ); + + require( + _oTokenAmount <= (totalBorrowable > oTokenAmountCustodied ? totalBorrowable.sub(oTokenAmountCustodied) : 0), + "MarginPool: Borrowing more than allocated" + ); + + uint256 collateralAssetAmount = _oTokenToCollateralAssetAmount(collateralAsset, _oTokenAmount); + + borrowed[msg.sender][collateralAsset] = outstandingAssetBorrow.add(collateralAssetAmount); + + // Decrease pool asset balance of collateralAsset + if (assetBalance[collateralAsset] > collateralAssetAmount) { + assetBalance[collateralAsset] = assetBalance[collateralAsset].sub(collateralAssetAmount); + } + + // transfer _oTokenAmount of oToken from borrower to _pool + ERC20Interface(_oToken).safeTransferFrom(msg.sender, address(this), _oTokenAmount); + // transfer collateralAssetAmount of collateralAsset from pool to borrower + ERC20Interface(collateralAsset).safeTransfer(msg.sender, collateralAssetAmount); + emit Borrow(_oToken, collateralAsset, collateralAssetAmount, msg.sender); + } + + /** + * @notice get the amount borrowable by market maker + * @param _borrower address of the borrower + * @param _oToken address of the oToken laying claim to the collateral assets + */ + function borrowable(address _borrower, address _oToken) external view returns (uint256) { + OtokenInterface oToken = OtokenInterface(_oToken); + address collateralAsset = oToken.collateralAsset(); + + uint256 outstandingAssetBorrow = borrowed[_borrower][collateralAsset]; + + uint256 collateralAllocatedToBorrower = _oTokenToCollateralAssetAmount( + collateralAsset, + ERC20Interface(_oToken).balanceOf(_borrower) + ); + + uint256 modifiedBal = collateralAllocatedToBorrower + .add(outstandingAssetBorrow) + .mul(borrowPCT[collateralAsset]) + .div(TOTAL_PCT); + return + whitelistedBorrower[_borrower] && + !oToken.isPut() && + oToken.expiryTimestamp() > block.timestamp && + modifiedBal > outstandingAssetBorrow + ? modifiedBal.sub(outstandingAssetBorrow) + : 0; + } + + /** + * @notice Repays asset back to pool before oToken expiry + * @param _oToken address of the oToken laying claim to the collateral assets + * @param _collateralAmount amount of the asset to repay + */ + function repay(address _oToken, uint256 _collateralAmount) public { + _repay(_oToken, _collateralAmount, msg.sender, msg.sender); + } + + /** + * @notice Repays asset back to pool for another borrower before oToken expiry + * @param _oToken address of the oToken laying claim to the collateral assets + * @param _collateralAmount amount of the asset to repay + * @param _borrower address of the borrower to repay for + */ + function repayFor( + address _oToken, + uint256 _collateralAmount, + address _borrower + ) public { + require(_borrower != address(0), "MarginPool: Borrower cannot be zero address"); + _repay(_oToken, _collateralAmount, _borrower, msg.sender); + } + + /** + * @notice Repays asset back to pool for another borrower before oToken expiry + * @param _oToken address of the oToken laying claim to the collateral assets + * @param _collateralAmount amount of the asset to repay + * @param _borrower address of the borrower to repay for + * @param _repayer address of the repayer of the loan + */ + function _repay( + address _oToken, + uint256 _collateralAmount, + address _borrower, + address _repayer + ) internal { + require(_collateralAmount > 0, "MarginPool: Cannot repay 0 of underlying"); + + address collateralAsset = OtokenInterface(_oToken).collateralAsset(); + uint256 outstandingAssetBorrow = borrowed[_borrower][collateralAsset]; + + require( + _collateralAmount <= outstandingAssetBorrow, + "MarginPool: Repaying more than outstanding borrow amount" + ); + + borrowed[_borrower][collateralAsset] = borrowed[_borrower][collateralAsset].sub(_collateralAmount); + + uint256 oTokensToRedeem = _collateralAssetToOTokenAmount(collateralAsset, _collateralAmount); + + // Increase pool asset balance of collateralAsset + assetBalance[collateralAsset] = assetBalance[collateralAsset].add(_collateralAmount); + + // transfer _amount of collateralAsset from borrower to pool + ERC20Interface(collateralAsset).safeTransferFrom(_repayer, address(this), _collateralAmount); + // transfer oTokensToRedeem of oToken from pool to _borrower + ERC20Interface(_oToken).safeTransfer(_borrower, oTokensToRedeem); + emit Repay(_oToken, collateralAsset, _collateralAmount, _borrower, _repayer); + } + + /** + * @notice Returns the equivalent in collateral asset amount (scale to `collateralAsset` decimals) + * @param _collateralAsset address of the collateral asset + * @param _amount amount to convert + */ + function _oTokenToCollateralAssetAmount(address _collateralAsset, uint256 _amount) internal view returns (uint256) { + uint256 collateralAssetDecimals = ERC20Interface(_collateralAsset).decimals(); + // If collateral asset has more decimals than oToken decimals (8), we scale up + // otherwise we scale down + return + collateralAssetDecimals >= 8 + ? _amount.mul(10**(collateralAssetDecimals.sub(8))) + : _amount.div(10**(uint256(8).sub(collateralAssetDecimals))); + } + + /** + * @notice Returns the equivalent in oToken amount (scale to 8 decimals) + * @param _collateralAsset address of the collateral asset + * @param _amount amount to convert + */ + function _collateralAssetToOTokenAmount(address _collateralAsset, uint256 _amount) internal view returns (uint256) { + uint256 collateralAssetDecimals = ERC20Interface(_collateralAsset).decimals(); + // If otoken has more decimals than collateral asset decimals, we scale down + // otherwise we scale up + return + collateralAssetDecimals >= 8 + ? _amount.div(10**(collateralAssetDecimals.sub(8))) + : _amount.mul(10**(uint256(8).sub(collateralAssetDecimals))); + } +} diff --git a/contracts/core/Controller.sol b/contracts/core/Controller.sol index 8a1d4d837..d08fbdc73 100644 --- a/contracts/core/Controller.sol +++ b/contracts/core/Controller.sol @@ -12,6 +12,7 @@ import {SafeMath} from "../packages/oz/SafeMath.sol"; import {MarginVault} from "../libs/MarginVault.sol"; import {Actions} from "../libs/Actions.sol"; import {AddressBookInterface} from "../interfaces/AddressBookInterface.sol"; +import {ERC20Interface} from "../interfaces/ERC20Interface.sol"; import {OtokenInterface} from "../interfaces/OtokenInterface.sol"; import {MarginCalculatorInterface} from "../interfaces/MarginCalculatorInterface.sol"; import {OracleInterface} from "../interfaces/OracleInterface.sol"; @@ -29,9 +30,6 @@ import {CalleeInterface} from "../interfaces/CalleeInterface.sol"; * C6: msg.sender is not authorized to run action * C7: invalid addressbook address * C8: invalid owner address - * C9: invalid input - * C10: fullPauser cannot be set to address zero - * C11: partialPauser cannot be set to address zero * C12: can not run actions for different owners * C13: can not run actions on different vaults * C14: invalid final vault state @@ -74,6 +72,7 @@ contract Controller is Initializable, OwnableUpgradeSafe, ReentrancyGuardUpgrade OracleInterface public oracle; MarginCalculatorInterface public calculator; MarginPoolInterface public pool; + MarginPoolInterface public borrowablePool; ///@dev scale used in MarginCalculator uint256 internal constant BASE = 8; @@ -103,7 +102,8 @@ contract Controller is Initializable, OwnableUpgradeSafe, ReentrancyGuardUpgrade /******************************************************************** V2.0.0 storage upgrade ******************************************************/ - /// @dev mapping to map vault by each vault type, naked margin vault should be set to 1, spread/max loss vault should be set to 0 + /// @dev mapping to map vault by each vault type + /// borrowable pool margin vault should be set to 2, naked margin vault should be set to 1, spread/max loss vault should be set to 0 mapping(address => mapping(uint256 => uint256)) internal vaultType; /// @dev mapping to store the timestamp at which the vault was last updated, will be updated in every action that changes the vault state or when calling sync() mapping(address => mapping(uint256 => uint256)) internal vaultLatestUpdate; @@ -324,14 +324,24 @@ contract Controller is Initializable, OwnableUpgradeSafe, ReentrancyGuardUpgrade emit Donated(msg.sender, _asset, _amount); } + /** + * @notice send asset amount to margin pool v2 + * @dev use donate() instead of direct transfer() to store the balance in assetBalance + * @param _asset asset address + * @param _amount amount to donate to pool + */ + function donateBorrowablePool(address _asset, uint256 _amount) external { + borrowablePool.transferToPool(_asset, msg.sender, _amount); + + emit Donated(msg.sender, _asset, _amount); + } + /** * @notice allows the partialPauser to toggle the systemPartiallyPaused variable and partially pause or partially unpause the system * @dev can only be called by the partialPauser * @param _partiallyPaused new boolean value to set systemPartiallyPaused to */ function setSystemPartiallyPaused(bool _partiallyPaused) external onlyPartialPauser { - require(systemPartiallyPaused != _partiallyPaused, "C9"); - systemPartiallyPaused = _partiallyPaused; emit SystemPartiallyPaused(systemPartiallyPaused); @@ -343,8 +353,6 @@ contract Controller is Initializable, OwnableUpgradeSafe, ReentrancyGuardUpgrade * @param _fullyPaused new boolean value to set systemFullyPaused to */ function setSystemFullyPaused(bool _fullyPaused) external onlyFullPauser { - require(systemFullyPaused != _fullyPaused, "C9"); - systemFullyPaused = _fullyPaused; emit SystemFullyPaused(systemFullyPaused); @@ -356,8 +364,6 @@ contract Controller is Initializable, OwnableUpgradeSafe, ReentrancyGuardUpgrade * @param _fullPauser new fullPauser address */ function setFullPauser(address _fullPauser) external onlyOwner { - require(_fullPauser != address(0), "C10"); - require(fullPauser != _fullPauser, "C9"); emit FullPauserUpdated(fullPauser, _fullPauser); fullPauser = _fullPauser; } @@ -368,8 +374,6 @@ contract Controller is Initializable, OwnableUpgradeSafe, ReentrancyGuardUpgrade * @param _partialPauser new partialPauser address */ function setPartialPauser(address _partialPauser) external onlyOwner { - require(_partialPauser != address(0), "C11"); - require(partialPauser != _partialPauser, "C9"); emit PartialPauserUpdated(partialPauser, _partialPauser); partialPauser = _partialPauser; } @@ -381,8 +385,6 @@ contract Controller is Initializable, OwnableUpgradeSafe, ReentrancyGuardUpgrade * @param _isRestricted new call restriction state */ function setCallRestriction(bool _isRestricted) external onlyOwner { - require(callRestricted != _isRestricted, "C9"); - callRestricted = _isRestricted; emit CallRestricted(callRestricted); @@ -395,8 +397,6 @@ contract Controller is Initializable, OwnableUpgradeSafe, ReentrancyGuardUpgrade * @param _isOperator new boolean value that expresses if the sender is giving or revoking privileges for _operator */ function setOperator(address _operator, bool _isOperator) external { - require(operators[msg.sender][_operator] != _isOperator, "C9"); - operators[msg.sender][_operator] = _isOperator; emit AccountOperatorUpdated(msg.sender, _operator, _isOperator); @@ -458,26 +458,6 @@ contract Controller is Initializable, OwnableUpgradeSafe, ReentrancyGuardUpgrade return operators[_owner][_operator]; } - /** - * @notice returns the current controller configuration - * @return whitelist, the address of the whitelist module - * @return oracle, the address of the oracle module - * @return calculator, the address of the calculator module - * @return pool, the address of the pool module - */ - function getConfiguration() - external - view - returns ( - address, - address, - address, - address - ) - { - return (address(whitelist), address(oracle), address(calculator), address(pool)); - } - /** * @notice return a vault's proceeds pre or post expiry, the amount of collateral that can be removed from a vault * @param _owner account owner of the vault @@ -718,7 +698,7 @@ contract Controller is Initializable, OwnableUpgradeSafe, ReentrancyGuardUpgrade // store new vault accountVaultCounter[_args.owner] = vaultId; - vaultType[_args.owner][vaultId] = _args.vaultType; + vaultType[_args.owner][vaultId] = borrowablePool.isWhitelistedOptionsVault(_args.owner) ? 2 : _args.vaultType; emit VaultOpened(_args.owner, vaultId, _args.vaultType); } @@ -745,7 +725,11 @@ contract Controller is Initializable, OwnableUpgradeSafe, ReentrancyGuardUpgrade vaults[_args.owner][_args.vaultId].addLong(_args.asset, _args.amount, _args.index); - pool.transferToPool(_args.asset, _args.from, _args.amount); + MarginPoolInterface(_getPool(vaultType[_args.owner][_args.vaultId])).transferToPool( + _args.asset, + _args.from, + _args.amount + ); emit LongOtokenDeposited(_args.asset, _args.owner, _args.from, _args.vaultId, _args.amount); } @@ -768,7 +752,11 @@ contract Controller is Initializable, OwnableUpgradeSafe, ReentrancyGuardUpgrade vaults[_args.owner][_args.vaultId].removeLong(_args.asset, _args.amount, _args.index); - pool.transferToUser(_args.asset, _args.to, _args.amount); + MarginPoolInterface(_getPool(vaultType[_args.owner][_args.vaultId])).transferToUser( + _args.asset, + _args.to, + _args.amount + ); emit LongOtokenWithdrawed(_args.asset, _args.owner, _args.to, _args.vaultId, _args.amount); } @@ -799,7 +787,11 @@ contract Controller is Initializable, OwnableUpgradeSafe, ReentrancyGuardUpgrade vaults[_args.owner][_args.vaultId].addCollateral(_args.asset, _args.amount, _args.index); - pool.transferToPool(_args.asset, _args.from, _args.amount); + MarginPoolInterface(_getPool(vaultType[_args.owner][_args.vaultId])).transferToPool( + _args.asset, + _args.from, + _args.amount + ); emit CollateralAssetDeposited(_args.asset, _args.owner, _args.from, _args.vaultId, _args.amount); } @@ -830,7 +822,11 @@ contract Controller is Initializable, OwnableUpgradeSafe, ReentrancyGuardUpgrade vaults[_args.owner][_args.vaultId].removeCollateral(_args.asset, _args.amount, _args.index); - pool.transferToUser(_args.asset, _args.to, _args.amount); + MarginPoolInterface(_getPool(vaultType[_args.owner][_args.vaultId])).transferToUser( + _args.asset, + _args.to, + _args.amount + ); emit CollateralAssetWithdrawed(_args.asset, _args.owner, _args.to, _args.vaultId, _args.amount); } @@ -910,7 +906,12 @@ contract Controller is Initializable, OwnableUpgradeSafe, ReentrancyGuardUpgrade otoken.burnOtoken(msg.sender, _args.amount); - pool.transferToUser(collateral, _args.receiver, payout); + MarginPoolInterface _pool = (!borrowablePool.isWhitelistedOTokenBuyer(msg.sender) || + ERC20Interface(collateral).balanceOf(address(borrowablePool)) < payout) + ? pool + : borrowablePool; + + _pool.transferToUser(collateral, _args.receiver, payout); emit Redeem(_args.otoken, msg.sender, _args.receiver, collateral, _args.amount, payout); } @@ -943,7 +944,7 @@ contract Controller is Initializable, OwnableUpgradeSafe, ReentrancyGuardUpgrade if (hasLong) { OtokenInterface longOtoken = OtokenInterface(vault.longOtokens[0]); - longOtoken.burnOtoken(address(pool), vault.longAmounts[0]); + longOtoken.burnOtoken(_getPool(vaultType[_args.owner][_args.vaultId]), vault.longAmounts[0]); } } @@ -965,7 +966,11 @@ contract Controller is Initializable, OwnableUpgradeSafe, ReentrancyGuardUpgrade nakedPoolBalance[collateral] = nakedPoolBalance[collateral].sub(payout); } - pool.transferToUser(collateral, _args.to, payout); + MarginPoolInterface(_getPool(vaultType[_args.owner][_args.vaultId])).transferToUser( + collateral, + _args.to, + payout + ); uint256 vaultId = _args.vaultId; address payoutRecipient = _args.to; @@ -1014,7 +1019,11 @@ contract Controller is Initializable, OwnableUpgradeSafe, ReentrancyGuardUpgrade // decrease internal naked margin collateral amount nakedPoolBalance[vault.collateralAssets[0]] = nakedPoolBalance[vault.collateralAssets[0]].sub(collateralToSell); - pool.transferToUser(vault.collateralAssets[0], _args.receiver, collateralToSell); + MarginPoolInterface(_getPool(vaultType[_args.owner][_args.vaultId])).transferToUser( + vault.collateralAssets[0], + _args.receiver, + collateralToSell + ); emit VaultLiquidated( msg.sender, @@ -1150,5 +1159,15 @@ contract Controller is Initializable, OwnableUpgradeSafe, ReentrancyGuardUpgrade oracle = OracleInterface(addressbook.getOracle()); calculator = MarginCalculatorInterface(addressbook.getMarginCalculator()); pool = MarginPoolInterface(addressbook.getMarginPool()); + borrowablePool = MarginPoolInterface(addressbook.getAddress(keccak256("BORROWABLE_POOL"))); + } + + /** + * @dev checks correct pool for vault type. type 0 or 1: non-borrowable pool, type 2: borrowable pool + * @param _vaultType type of the vault + * @return margin pool corresponding to the vault + */ + function _getPool(uint256 _vaultType) internal view returns (address) { + return _vaultType < 2 ? address(pool) : address(borrowablePool); } } diff --git a/contracts/interfaces/MarginPoolInterface.sol b/contracts/interfaces/MarginPoolInterface.sol index 7b3cfd6e5..7a1aca185 100644 --- a/contracts/interfaces/MarginPoolInterface.sol +++ b/contracts/interfaces/MarginPoolInterface.sol @@ -9,6 +9,10 @@ interface MarginPoolInterface { function getStoredBalance(address _asset) external view returns (uint256); + function isWhitelistedOTokenBuyer(address _oTokenBuyer) external view returns (bool); + + function isWhitelistedOptionsVault(address _optionsVault) external view returns (bool); + /* Admin-only functions */ function setFarmer(address _farmer) external; diff --git a/test/integration-tests/longCallSpreadExpireItm.test.ts b/test/integration-tests/longCallSpreadExpireItm.test.ts index 04d249f51..ea90ad10e 100644 --- a/test/integration-tests/longCallSpreadExpireItm.test.ts +++ b/test/integration-tests/longCallSpreadExpireItm.test.ts @@ -7,6 +7,7 @@ import { ControllerInstance, WhitelistInstance, MarginPoolInstance, + BorrowableMarginPoolInstance, OtokenFactoryInstance, } from '../../build/types/truffle-types' import { createTokenAmount, createValidExpiry } from '../utils' @@ -20,6 +21,7 @@ const MockERC20 = artifacts.require('MockERC20.sol') const MarginCalculator = artifacts.require('MarginCalculator.sol') const Whitelist = artifacts.require('Whitelist.sol') const MarginPool = artifacts.require('MarginPool.sol') +const BorrowableMarginPool = artifacts.require('BorrowableMarginPool.sol') const Controller = artifacts.require('Controller.sol') const MarginVault = artifacts.require('MarginVault.sol') const OTokenFactory = artifacts.require('OtokenFactory.sol') @@ -46,6 +48,7 @@ contract('Long Call Spread Option expires Itm flow', ([accountOwner1, nakedBuyer let controllerProxy: ControllerInstance let controllerImplementation: ControllerInstance let marginPool: MarginPoolInstance + let borrowableMarginPool: MarginPoolInstance let whitelist: WhitelistInstance let otokenImplementation: OtokenInstance let otokenFactory: OtokenFactoryInstance @@ -82,8 +85,11 @@ contract('Long Call Spread Option expires Itm flow', ([accountOwner1, nakedBuyer addressBook = await AddressBook.new() // setup margin pool marginPool = await MarginPool.new(addressBook.address) + // setup margin pool v2 + borrowableMarginPool = await BorrowableMarginPool.new(addressBook.address) // setup margin vault const lib = await MarginVault.new() + // setup controllerProxy module await Controller.link('MarginVault', lib.address) controllerImplementation = await Controller.new(addressBook.address) @@ -107,6 +113,9 @@ contract('Long Call Spread Option expires Itm flow', ([accountOwner1, nakedBuyer await addressBook.setMarginCalculator(calculator.address) await addressBook.setWhitelist(whitelist.address) await addressBook.setMarginPool(marginPool.address) + await addressBook.setAddress(web3.utils.soliditySha3('BORROWABLE_POOL'), borrowableMarginPool.address, { + from: accountOwner1, + }) await addressBook.setOtokenFactory(otokenFactory.address) await addressBook.setOtokenImpl(otokenImplementation.address) await addressBook.setController(controllerImplementation.address) @@ -114,6 +123,8 @@ contract('Long Call Spread Option expires Itm flow', ([accountOwner1, nakedBuyer const controllerProxyAddress = await addressBook.getController() controllerProxy = await Controller.at(controllerProxyAddress) + // await controllerProxy.refreshConfiguration(); + await otokenFactory.createOtoken( weth.address, usdc.address, diff --git a/test/integration-tests/longCallSpreadExpireOtm.test.ts b/test/integration-tests/longCallSpreadExpireOtm.test.ts index bf4c0bc1a..ac98f37aa 100644 --- a/test/integration-tests/longCallSpreadExpireOtm.test.ts +++ b/test/integration-tests/longCallSpreadExpireOtm.test.ts @@ -7,6 +7,7 @@ import { ControllerInstance, WhitelistInstance, MarginPoolInstance, + BorrowableMarginPoolInstance, OtokenFactoryInstance, } from '../../build/types/truffle-types' import { createTokenAmount, createValidExpiry } from '../utils' @@ -20,6 +21,7 @@ const MockERC20 = artifacts.require('MockERC20.sol') const MarginCalculator = artifacts.require('MarginCalculator.sol') const Whitelist = artifacts.require('Whitelist.sol') const MarginPool = artifacts.require('MarginPool.sol') +const BorrowableMarginPool = artifacts.require('BorrowableMarginPool.sol') const Controller = artifacts.require('Controller.sol') const MarginVault = artifacts.require('MarginVault.sol') const OTokenFactory = artifacts.require('OtokenFactory.sol') @@ -46,6 +48,7 @@ contract('Long Call Spread Option expires Otm flow', ([accountOwner1, nakedBuyer let controllerProxy: ControllerInstance let controllerImplementation: ControllerInstance let marginPool: MarginPoolInstance + let borrowableMarginPool: BorrowableMarginPoolInstance let whitelist: WhitelistInstance let otokenImplementation: OtokenInstance let otokenFactory: OtokenFactoryInstance @@ -81,6 +84,8 @@ contract('Long Call Spread Option expires Otm flow', ([accountOwner1, nakedBuyer addressBook = await AddressBook.new() // setup margin pool marginPool = await MarginPool.new(addressBook.address) + // setup margin pool v2 + borrowableMarginPool = await BorrowableMarginPool.new(addressBook.address) // setup margin vault const lib = await MarginVault.new() // setup controllerProxy module @@ -106,6 +111,9 @@ contract('Long Call Spread Option expires Otm flow', ([accountOwner1, nakedBuyer await addressBook.setMarginCalculator(calculator.address) await addressBook.setWhitelist(whitelist.address) await addressBook.setMarginPool(marginPool.address) + await addressBook.setAddress(web3.utils.soliditySha3('BORROWABLE_POOL'), borrowableMarginPool.address, { + from: accountOwner1, + }) await addressBook.setOtokenFactory(otokenFactory.address) await addressBook.setOtokenImpl(otokenImplementation.address) await addressBook.setController(controllerImplementation.address) diff --git a/test/integration-tests/longCallSpreadPreExpiry.test.ts b/test/integration-tests/longCallSpreadPreExpiry.test.ts index 274234046..cb0367021 100644 --- a/test/integration-tests/longCallSpreadPreExpiry.test.ts +++ b/test/integration-tests/longCallSpreadPreExpiry.test.ts @@ -7,6 +7,7 @@ import { ControllerInstance, WhitelistInstance, MarginPoolInstance, + BorrowableMarginPoolInstance, OtokenFactoryInstance, } from '../../build/types/truffle-types' import { createTokenAmount, createValidExpiry } from '../utils' @@ -20,6 +21,7 @@ const MockERC20 = artifacts.require('MockERC20.sol') const MarginCalculator = artifacts.require('MarginCalculator.sol') const Whitelist = artifacts.require('Whitelist.sol') const MarginPool = artifacts.require('MarginPool.sol') +const BorrowableMarginPool = artifacts.require('BorrowableMarginPool.sol') const Controller = artifacts.require('Controller.sol') const MarginVault = artifacts.require('MarginVault.sol') const OTokenFactory = artifacts.require('OtokenFactory.sol') @@ -46,6 +48,7 @@ contract('Long Call Spread Option closed before expiry flow', ([accountOwner1, n let controllerProxy: ControllerInstance let controllerImplementation: ControllerInstance let marginPool: MarginPoolInstance + let borrowableMarginPool: MarginPoolInstance let whitelist: WhitelistInstance let otokenImplementation: OtokenInstance let otokenFactory: OtokenFactoryInstance @@ -82,6 +85,8 @@ contract('Long Call Spread Option closed before expiry flow', ([accountOwner1, n addressBook = await AddressBook.new() // setup margin pool marginPool = await MarginPool.new(addressBook.address) + // setup margin pool v2 + borrowableMarginPool = await BorrowableMarginPool.new(addressBook.address) // setup margin vault const lib = await MarginVault.new() // setup controllerProxy module @@ -107,6 +112,9 @@ contract('Long Call Spread Option closed before expiry flow', ([accountOwner1, n await addressBook.setMarginCalculator(calculator.address) await addressBook.setWhitelist(whitelist.address) await addressBook.setMarginPool(marginPool.address) + await addressBook.setAddress(web3.utils.soliditySha3('BORROWABLE_POOL'), borrowableMarginPool.address, { + from: accountOwner1, + }) await addressBook.setOtokenFactory(otokenFactory.address) await addressBook.setOtokenImpl(otokenImplementation.address) await addressBook.setController(controllerImplementation.address) diff --git a/test/integration-tests/longPutExpireItm.test.ts b/test/integration-tests/longPutExpireItm.test.ts index 17c1f55c3..59f6685bc 100644 --- a/test/integration-tests/longPutExpireItm.test.ts +++ b/test/integration-tests/longPutExpireItm.test.ts @@ -7,6 +7,7 @@ import { ControllerInstance, WhitelistInstance, MarginPoolInstance, + BorrowableMarginPoolInstance, OtokenFactoryInstance, } from '../../build/types/truffle-types' import { createTokenAmount, createValidExpiry } from '../utils' @@ -20,6 +21,7 @@ const MockERC20 = artifacts.require('MockERC20.sol') const MarginCalculator = artifacts.require('MarginCalculator.sol') const Whitelist = artifacts.require('Whitelist.sol') const MarginPool = artifacts.require('MarginPool.sol') +const BorrowableMarginPool = artifacts.require('BorrowableMarginPool.sol') const Controller = artifacts.require('Controller.sol') const MarginVault = artifacts.require('MarginVault.sol') const OTokenFactory = artifacts.require('OtokenFactory.sol') @@ -46,6 +48,7 @@ contract('Long Put Spread Option closed ITM flow', ([accountOwner1, accountOwner let controllerProxy: ControllerInstance let controllerImplementation: ControllerInstance let marginPool: MarginPoolInstance + let borrowableMarginPool: MarginPoolInstance let whitelist: WhitelistInstance let otokenImplementation: OtokenInstance let otokenFactory: OtokenFactoryInstance @@ -82,6 +85,8 @@ contract('Long Put Spread Option closed ITM flow', ([accountOwner1, accountOwner addressBook = await AddressBook.new() // setup margin pool marginPool = await MarginPool.new(addressBook.address) + // setup margin pool v2 + borrowableMarginPool = await BorrowableMarginPool.new(addressBook.address) // setup margin vault const lib = await MarginVault.new() // setup controllerProxy module @@ -107,6 +112,9 @@ contract('Long Put Spread Option closed ITM flow', ([accountOwner1, accountOwner await addressBook.setMarginCalculator(calculator.address) await addressBook.setWhitelist(whitelist.address) await addressBook.setMarginPool(marginPool.address) + await addressBook.setAddress(web3.utils.soliditySha3('BORROWABLE_POOL'), borrowableMarginPool.address, { + from: accountOwner1, + }) await addressBook.setOtokenFactory(otokenFactory.address) await addressBook.setOtokenImpl(otokenImplementation.address) await addressBook.setController(controllerImplementation.address) diff --git a/test/integration-tests/longPutExpireOtm.test.ts b/test/integration-tests/longPutExpireOtm.test.ts index 2f2f4017b..44270c23a 100644 --- a/test/integration-tests/longPutExpireOtm.test.ts +++ b/test/integration-tests/longPutExpireOtm.test.ts @@ -7,6 +7,7 @@ import { ControllerInstance, WhitelistInstance, MarginPoolInstance, + BorrowableMarginPoolInstance, OtokenFactoryInstance, } from '../../build/types/truffle-types' import { createTokenAmount, createValidExpiry } from '../utils' @@ -20,6 +21,7 @@ const MockERC20 = artifacts.require('MockERC20.sol') const MarginCalculator = artifacts.require('MarginCalculator.sol') const Whitelist = artifacts.require('Whitelist.sol') const MarginPool = artifacts.require('MarginPool.sol') +const BorrowableMarginPool = artifacts.require('BorrowableMarginPool.sol') const Controller = artifacts.require('Controller.sol') const MarginVault = artifacts.require('MarginVault.sol') const OTokenFactory = artifacts.require('OtokenFactory.sol') @@ -46,6 +48,7 @@ contract('Long Put Spread Option expires Otm flow', ([accountOwner1, nakedBuyer, let controllerProxy: ControllerInstance let controllerImplementation: ControllerInstance let marginPool: MarginPoolInstance + let borrowableMarginPool: MarginPoolInstance let whitelist: WhitelistInstance let otokenImplementation: OtokenInstance let otokenFactory: OtokenFactoryInstance @@ -82,6 +85,8 @@ contract('Long Put Spread Option expires Otm flow', ([accountOwner1, nakedBuyer, addressBook = await AddressBook.new() // setup margin pool marginPool = await MarginPool.new(addressBook.address) + // setup margin pool v2 + borrowableMarginPool = await BorrowableMarginPool.new(addressBook.address) // setup margin vault const lib = await MarginVault.new() // setup controllerProxy module @@ -107,6 +112,9 @@ contract('Long Put Spread Option expires Otm flow', ([accountOwner1, nakedBuyer, await addressBook.setMarginCalculator(calculator.address) await addressBook.setWhitelist(whitelist.address) await addressBook.setMarginPool(marginPool.address) + await addressBook.setAddress(web3.utils.soliditySha3('BORROWABLE_POOL'), borrowableMarginPool.address, { + from: accountOwner1, + }) await addressBook.setOtokenFactory(otokenFactory.address) await addressBook.setOtokenImpl(otokenImplementation.address) await addressBook.setController(controllerImplementation.address) diff --git a/test/integration-tests/longPutPreExpiry.test.ts b/test/integration-tests/longPutPreExpiry.test.ts index 8b0ed74ad..cfbe9f69c 100644 --- a/test/integration-tests/longPutPreExpiry.test.ts +++ b/test/integration-tests/longPutPreExpiry.test.ts @@ -7,6 +7,7 @@ import { ControllerInstance, WhitelistInstance, MarginPoolInstance, + BorrowableMarginPoolInstance, OtokenFactoryInstance, } from '../../build/types/truffle-types' import { createTokenAmount, createValidExpiry } from '../utils' @@ -20,6 +21,7 @@ const MockERC20 = artifacts.require('MockERC20.sol') const MarginCalculator = artifacts.require('MarginCalculator.sol') const Whitelist = artifacts.require('Whitelist.sol') const MarginPool = artifacts.require('MarginPool.sol') +const BorrowableMarginPool = artifacts.require('BorrowableMarginPool.sol') const Controller = artifacts.require('Controller.sol') const MarginVault = artifacts.require('MarginVault.sol') const OTokenFactory = artifacts.require('OtokenFactory.sol') @@ -46,6 +48,7 @@ contract('Long Put Spread Option closed before expiry flow', ([accountOwner1, bu let controllerProxy: ControllerInstance let controllerImplementation: ControllerInstance let marginPool: MarginPoolInstance + let borrowableMarginPool: MarginPoolInstance let whitelist: WhitelistInstance let otokenImplementation: OtokenInstance let otokenFactory: OtokenFactoryInstance @@ -82,6 +85,8 @@ contract('Long Put Spread Option closed before expiry flow', ([accountOwner1, bu addressBook = await AddressBook.new() // setup margin pool marginPool = await MarginPool.new(addressBook.address) + // setup margin pool v2 + borrowableMarginPool = await BorrowableMarginPool.new(addressBook.address) // setup margin vault const lib = await MarginVault.new() // setup controllerProxy module @@ -107,6 +112,9 @@ contract('Long Put Spread Option closed before expiry flow', ([accountOwner1, bu await addressBook.setMarginCalculator(calculator.address) await addressBook.setWhitelist(whitelist.address) await addressBook.setMarginPool(marginPool.address) + await addressBook.setAddress(web3.utils.soliditySha3('BORROWABLE_POOL'), borrowableMarginPool.address, { + from: accountOwner1, + }) await addressBook.setOtokenFactory(otokenFactory.address) await addressBook.setOtokenImpl(otokenImplementation.address) await addressBook.setController(controllerImplementation.address) diff --git a/test/integration-tests/nakedCallExpireItm-borrowableMarginPool.test.ts b/test/integration-tests/nakedCallExpireItm-borrowableMarginPool.test.ts new file mode 100644 index 000000000..54f3f2f34 --- /dev/null +++ b/test/integration-tests/nakedCallExpireItm-borrowableMarginPool.test.ts @@ -0,0 +1,328 @@ +import { + MockERC20Instance, + MarginCalculatorInstance, + AddressBookInstance, + MockOracleInstance, + OtokenInstance, + ControllerInstance, + WhitelistInstance, + MarginPoolInstance, + BorrowableMarginPoolInstance, + OtokenFactoryInstance, +} from '../../build/types/truffle-types' +import { createTokenAmount, createValidExpiry } from '../utils' +import BigNumber from 'bignumber.js' + +const { time } = require('@openzeppelin/test-helpers') +const AddressBook = artifacts.require('AddressBook.sol') +const MockOracle = artifacts.require('MockOracle.sol') +const Otoken = artifacts.require('Otoken.sol') +const MockERC20 = artifacts.require('MockERC20.sol') +const MarginCalculator = artifacts.require('MarginCalculator.sol') +const Whitelist = artifacts.require('Whitelist.sol') +const MarginPool = artifacts.require('MarginPool.sol') +const BorrowableMarginPool = artifacts.require('BorrowableMarginPool.sol') +const Controller = artifacts.require('Controller.sol') +const MarginVault = artifacts.require('MarginVault.sol') +const OTokenFactory = artifacts.require('OtokenFactory.sol') +const ZERO_ADDR = '0x0000000000000000000000000000000000000000' + +enum ActionType { + OpenVault, + MintShortOption, + BurnShortOption, + DepositLongOption, + WithdrawLongOption, + DepositCollateral, + WithdrawCollateral, + SettleVault, + Redeem, + Call, +} + +contract('Naked Call Option expires Itm flow', ([accountOwner1, buyer]) => { + let expiry: number + + let addressBook: AddressBookInstance + let calculator: MarginCalculatorInstance + let controllerProxy: ControllerInstance + let controllerImplementation: ControllerInstance + let marginPool: MarginPoolInstance + let borrowableMarginPool: BorrowableMarginPoolInstance + let whitelist: WhitelistInstance + let otokenImplementation: OtokenInstance + let otokenFactory: OtokenFactoryInstance + + // oracle modulce mock + let oracle: MockOracleInstance + + let usdc: MockERC20Instance + let weth: MockERC20Instance + + let ethCall: OtokenInstance + const strikePrice = 300 + + const optionsAmount = 10 + const collateralAmount = optionsAmount + let vaultCounter: number + + const usdcDecimals = 6 + const wethDecimals = 18 + + before('set up contracts', async () => { + const now = (await time.latest()).toNumber() + expiry = createValidExpiry(now, 30) + + // setup usdc and weth + usdc = await MockERC20.new('USDC', 'USDC', usdcDecimals) + weth = await MockERC20.new('WETH', 'WETH', wethDecimals) + // initiate addressbook first. + addressBook = await AddressBook.new() + // setup margin pool + marginPool = await MarginPool.new(addressBook.address) + // setup margin pool v2 + borrowableMarginPool = await BorrowableMarginPool.new(addressBook.address) + // setup margin vault + const lib = await MarginVault.new() + // setup controllerProxy module + await Controller.link('MarginVault', lib.address) + controllerImplementation = await Controller.new(addressBook.address) + // setup mock Oracle module + oracle = await MockOracle.new(addressBook.address) + // setup calculator + calculator = await MarginCalculator.new(oracle.address) + // setup whitelist module + whitelist = await Whitelist.new(addressBook.address) + await whitelist.whitelistCollateral(weth.address) + await whitelist.whitelistCollateral(usdc.address) + whitelist.whitelistProduct(weth.address, usdc.address, usdc.address, true) + whitelist.whitelistProduct(weth.address, usdc.address, weth.address, false) + // setup otoken + otokenImplementation = await Otoken.new() + // setup factory + otokenFactory = await OTokenFactory.new(addressBook.address) + + // setup address book + await addressBook.setOracle(oracle.address) + await addressBook.setMarginCalculator(calculator.address) + await addressBook.setWhitelist(whitelist.address) + await addressBook.setMarginPool(marginPool.address) + await addressBook.setAddress(web3.utils.soliditySha3('BORROWABLE_POOL'), borrowableMarginPool.address, { + from: accountOwner1, + }) + await addressBook.setOtokenFactory(otokenFactory.address) + await addressBook.setOtokenImpl(otokenImplementation.address) + await addressBook.setController(controllerImplementation.address) + + const controllerProxyAddress = await addressBook.getController() + controllerProxy = await Controller.at(controllerProxyAddress) + + await otokenFactory.createOtoken( + weth.address, + usdc.address, + weth.address, + createTokenAmount(strikePrice), + expiry, + false, + ) + + const ethCallAddress = await otokenFactory.getOtoken( + weth.address, + usdc.address, + weth.address, + createTokenAmount(strikePrice), + expiry, + false, + ) + + ethCall = await Otoken.at(ethCallAddress) + // mint weth to user + const account1OwnerWeth = createTokenAmount(2 * collateralAmount, wethDecimals) + await weth.mint(accountOwner1, account1OwnerWeth) + + // have the user approve all the weth transfers + await weth.approve(borrowableMarginPool.address, account1OwnerWeth, { from: accountOwner1 }) + + const vaultCounterBefore = new BigNumber(await controllerProxy.getAccountVaultCounter(accountOwner1)) + vaultCounter = vaultCounterBefore.toNumber() + 1 + + await borrowableMarginPool.setOptionsVaultWhitelistedStatus(accountOwner1, true, { from: accountOwner1 }) + await borrowableMarginPool.setOTokenBuyerWhitelistedStatus(buyer, true, { from: accountOwner1 }) + }) + + describe('Integration test: Close a naked call after it expires ITM', () => { + const scaledOptionsAmount = createTokenAmount(optionsAmount) + const scaledCollateralAmount = createTokenAmount(collateralAmount, wethDecimals) + const expirySpotPrice = 400 + before('Seller should be able to open a short call option', async () => { + const actionArgs = [ + { + actionType: ActionType.OpenVault, + owner: accountOwner1, + secondAddress: accountOwner1, + asset: ZERO_ADDR, + vaultId: vaultCounter, + amount: '0', + index: '0', + data: ZERO_ADDR, + }, + { + actionType: ActionType.MintShortOption, + owner: accountOwner1, + secondAddress: accountOwner1, + asset: ethCall.address, + vaultId: vaultCounter, + amount: scaledOptionsAmount, + index: '0', + data: ZERO_ADDR, + }, + { + actionType: ActionType.DepositCollateral, + owner: accountOwner1, + secondAddress: accountOwner1, + asset: weth.address, + vaultId: vaultCounter, + amount: scaledCollateralAmount, + index: '0', + data: ZERO_ADDR, + }, + ] + + await controllerProxy.operate(actionArgs, { from: accountOwner1 }) + }) + + it('Seller: close an ITM position after expiry', async () => { + // Keep track of balances before + const ownerWethBalanceBefore = new BigNumber(await weth.balanceOf(accountOwner1)) + const borrowableMarginPoolWethBalanceBefore = new BigNumber(await weth.balanceOf(borrowableMarginPool.address)) + const ownerOtokenBalanceBefore = new BigNumber(await ethCall.balanceOf(accountOwner1)) + const oTokenSupplyBefore = new BigNumber(await ethCall.totalSupply()) + + // Check that we start at a valid state + const vaultBefore = await controllerProxy.getVaultWithDetails(accountOwner1, vaultCounter) + const vaultStateBefore = await calculator.getExcessCollateral(vaultBefore[0], vaultBefore[1]) + assert.equal(vaultStateBefore[0].toString(), '0') + assert.equal(vaultStateBefore[1], true) + + // Set the oracle price + if ((await time.latest()) < expiry) { + await time.increaseTo(expiry + 2) + } + const strikePriceChange = Math.max(expirySpotPrice - strikePrice, 0) + const scaledETHPrice = createTokenAmount(expirySpotPrice, 8) + const scaledUSDCPrice = createTokenAmount(1) + await oracle.setExpiryPriceFinalizedAllPeiodOver(weth.address, expiry, scaledETHPrice, true) + await oracle.setExpiryPriceFinalizedAllPeiodOver(usdc.address, expiry, scaledUSDCPrice, true) + + const collateralPayout = collateralAmount - (strikePriceChange * optionsAmount) / expirySpotPrice + + // Check that after expiry, the vault excess balance has updated as expected + const vaultStateBeforeSettlement = await calculator.getExcessCollateral(vaultBefore[0], vaultBefore[1]) + assert.equal( + vaultStateBeforeSettlement[0].toString(), + createTokenAmount(collateralPayout, wethDecimals), + 'vault before settlement collateral excess mismatch', + ) + assert.equal(vaultStateBeforeSettlement[1], true) + + const actionArgs = [ + { + actionType: ActionType.SettleVault, + owner: accountOwner1, + secondAddress: accountOwner1, + asset: ZERO_ADDR, + vaultId: vaultCounter, + amount: '0', + index: '0', + data: ZERO_ADDR, + }, + ] + + await controllerProxy.operate(actionArgs, { from: accountOwner1 }) + + // keep track of balances after + const ownerWethBalanceAfter = new BigNumber(await weth.balanceOf(accountOwner1)) + const borrowableMarginPoolWethBalanceAfter = new BigNumber(await weth.balanceOf(borrowableMarginPool.address)) + + const ownerOtokenBalanceAfter = new BigNumber(await ethCall.balanceOf(accountOwner1)) + const oTokenSupplyAfter = new BigNumber(await ethCall.totalSupply()) + + // check balances before and after changed as expected + assert.equal( + ownerWethBalanceBefore.plus(createTokenAmount(collateralPayout, wethDecimals)).toString(), + ownerWethBalanceAfter.toString(), + ) + assert.equal( + borrowableMarginPoolWethBalanceBefore.minus(createTokenAmount(collateralPayout, wethDecimals)).toString(), + borrowableMarginPoolWethBalanceAfter.toString(), + ) + assert.equal(ownerOtokenBalanceBefore.toString(), ownerOtokenBalanceAfter.toString()) + assert.equal(oTokenSupplyBefore.toString(), oTokenSupplyAfter.toString()) + + // Check that we end at a valid state + const vaultAfter = await controllerProxy.getVaultWithDetails(accountOwner1, vaultCounter) + const vaultStateAfter = await calculator.getExcessCollateral(vaultAfter[0], vaultAfter[1]) + assert.equal(vaultStateAfter[0].toString(), '0') + assert.equal(vaultStateAfter[1], true) + + // Check the vault balances stored in the contract + assert.equal(vaultAfter[0].shortOtokens.length, 0, 'Length of the short otoken array in the vault is incorrect') + assert.equal(vaultAfter[0].collateralAssets.length, 0, 'Length of the collateral array in the vault is incorrect') + assert.equal(vaultAfter[0].longOtokens.length, 0, 'Length of the long otoken array in the vault is incorrect') + + assert.equal(vaultAfter[0].shortAmounts.length, 0, 'Length of the short amounts array in the vault is incorrect') + assert.equal( + vaultAfter[0].collateralAmounts.length, + 0, + 'Length of the collateral amounts array in the vault is incorrect', + ) + assert.equal(vaultAfter[0].longAmounts.length, 0, 'Length of the long amounts array in the vault is incorrect') + }) + + it('Buyer: redeem ITM call option after expiry', async () => { + // owner sells their call option + await ethCall.transfer(buyer, scaledOptionsAmount, { from: accountOwner1 }) + // oracle orice increases + const strikePriceChange = Math.max(expirySpotPrice - strikePrice, 0) + + // Keep track of balances before + const ownerWethBalanceBefore = new BigNumber(await weth.balanceOf(buyer)) + const borrowableMarginPoolWethBalanceBefore = new BigNumber(await weth.balanceOf(borrowableMarginPool.address)) + const ownerOtokenBalanceBefore = new BigNumber(await ethCall.balanceOf(buyer)) + const oTokenSupplyBefore = new BigNumber(await ethCall.totalSupply()) + + const actionArgs = [ + { + actionType: ActionType.Redeem, + owner: buyer, + secondAddress: buyer, + asset: ethCall.address, + vaultId: '0', + amount: scaledOptionsAmount, + index: '0', + data: ZERO_ADDR, + }, + ] + + await controllerProxy.operate(actionArgs, { from: buyer }) + + // keep track of balances after + const ownerWethBalanceAfter = new BigNumber(await weth.balanceOf(buyer)) + const borrowableMarginPoolWethBalanceAfter = new BigNumber(await weth.balanceOf(borrowableMarginPool.address)) + const ownerOtokenBalanceAfter = new BigNumber(await ethCall.balanceOf(buyer)) + const oTokenSupplyAfter = new BigNumber(await ethCall.totalSupply()) + + const payout = (strikePriceChange * optionsAmount) / expirySpotPrice + const scaledPayout = createTokenAmount(payout, wethDecimals) + + // check balances before and after changed as expected + assert.equal(ownerWethBalanceBefore.plus(scaledPayout).toString(), ownerWethBalanceAfter.toString()) + assert.equal( + borrowableMarginPoolWethBalanceBefore.minus(scaledPayout).toString(), + borrowableMarginPoolWethBalanceAfter.toString(), + ) + assert.equal(ownerOtokenBalanceBefore.minus(scaledOptionsAmount).toString(), ownerOtokenBalanceAfter.toString()) + assert.equal(oTokenSupplyBefore.minus(scaledOptionsAmount).toString(), oTokenSupplyAfter.toString()) + }) + }) +}) diff --git a/test/integration-tests/nakedCallExpireItm.test.ts b/test/integration-tests/nakedCallExpireItm.test.ts index 5447bab44..8a8301cfc 100644 --- a/test/integration-tests/nakedCallExpireItm.test.ts +++ b/test/integration-tests/nakedCallExpireItm.test.ts @@ -7,6 +7,7 @@ import { ControllerInstance, WhitelistInstance, MarginPoolInstance, + BorrowableMarginPoolInstance, OtokenFactoryInstance, } from '../../build/types/truffle-types' import { createTokenAmount, createValidExpiry } from '../utils' @@ -20,6 +21,7 @@ const MockERC20 = artifacts.require('MockERC20.sol') const MarginCalculator = artifacts.require('MarginCalculator.sol') const Whitelist = artifacts.require('Whitelist.sol') const MarginPool = artifacts.require('MarginPool.sol') +const BorrowableMarginPool = artifacts.require('BorrowableMarginPool.sol') const Controller = artifacts.require('Controller.sol') const MarginVault = artifacts.require('MarginVault.sol') const OTokenFactory = artifacts.require('OtokenFactory.sol') @@ -46,6 +48,7 @@ contract('Naked Call Option expires Itm flow', ([accountOwner1, buyer]) => { let controllerProxy: ControllerInstance let controllerImplementation: ControllerInstance let marginPool: MarginPoolInstance + let borrowableMarginPool: MarginPoolInstance let whitelist: WhitelistInstance let otokenImplementation: OtokenInstance let otokenFactory: OtokenFactoryInstance @@ -77,6 +80,8 @@ contract('Naked Call Option expires Itm flow', ([accountOwner1, buyer]) => { addressBook = await AddressBook.new() // setup margin pool marginPool = await MarginPool.new(addressBook.address) + // setup margin pool v2 + borrowableMarginPool = await BorrowableMarginPool.new(addressBook.address) // setup margin vault const lib = await MarginVault.new() // setup controllerProxy module @@ -102,6 +107,9 @@ contract('Naked Call Option expires Itm flow', ([accountOwner1, buyer]) => { await addressBook.setMarginCalculator(calculator.address) await addressBook.setWhitelist(whitelist.address) await addressBook.setMarginPool(marginPool.address) + await addressBook.setAddress(web3.utils.soliditySha3('BORROWABLE_POOL'), borrowableMarginPool.address, { + from: accountOwner1, + }) await addressBook.setOtokenFactory(otokenFactory.address) await addressBook.setOtokenImpl(otokenImplementation.address) await addressBook.setController(controllerImplementation.address) diff --git a/test/integration-tests/nakedCallExpireOtm-borrowableMarginPool.test.ts b/test/integration-tests/nakedCallExpireOtm-borrowableMarginPool.test.ts new file mode 100644 index 000000000..a05237d42 --- /dev/null +++ b/test/integration-tests/nakedCallExpireOtm-borrowableMarginPool.test.ts @@ -0,0 +1,310 @@ +import { + MockERC20Instance, + MarginCalculatorInstance, + AddressBookInstance, + MockOracleInstance, + OtokenInstance, + ControllerInstance, + WhitelistInstance, + MarginPoolInstance, + BorrowableMarginPoolInstance, + OtokenFactoryInstance, +} from '../../build/types/truffle-types' +import { createTokenAmount, createValidExpiry } from '../utils' +import BigNumber from 'bignumber.js' + +const { time } = require('@openzeppelin/test-helpers') +const AddressBook = artifacts.require('AddressBook.sol') +const MockOracle = artifacts.require('MockOracle.sol') +const Otoken = artifacts.require('Otoken.sol') +const MockERC20 = artifacts.require('MockERC20.sol') +const MarginCalculator = artifacts.require('MarginCalculator.sol') +const Whitelist = artifacts.require('Whitelist.sol') +const MarginPool = artifacts.require('MarginPool.sol') +const BorrowableMarginPool = artifacts.require('BorrowableMarginPool.sol') +const Controller = artifacts.require('Controller.sol') +const MarginVault = artifacts.require('MarginVault.sol') +const OTokenFactory = artifacts.require('OtokenFactory.sol') +const ZERO_ADDR = '0x0000000000000000000000000000000000000000' + +enum ActionType { + OpenVault, + MintShortOption, + BurnShortOption, + DepositLongOption, + WithdrawLongOption, + DepositCollateral, + WithdrawCollateral, + SettleVault, + Redeem, + Call, +} + +contract('Naked Call Option expires Otm flow', ([accountOwner1, buyer]) => { + let expiry: number + + let addressBook: AddressBookInstance + let calculator: MarginCalculatorInstance + let controllerProxy: ControllerInstance + let controllerImplementation: ControllerInstance + let marginPool: MarginPoolInstance + let borrowableMarginPool: BorrowableMarginPoolInstance + let whitelist: WhitelistInstance + let otokenImplementation: OtokenInstance + let otokenFactory: OtokenFactoryInstance + + // oracle modulce mock + let oracle: MockOracleInstance + + let usdc: MockERC20Instance + let weth: MockERC20Instance + + let ethCall: OtokenInstance + const strikePrice = 300 + + const optionsAmount = 10 + const collateralAmount = optionsAmount + let vaultCounter: number + + const usdcDecimals = 6 + const wethDecimals = 18 + + before('set up contracts', async () => { + const now = (await time.latest()).toNumber() + expiry = createValidExpiry(now, 30) + + // setup usdc and weth + usdc = await MockERC20.new('USDC', 'USDC', usdcDecimals) + weth = await MockERC20.new('WETH', 'WETH', wethDecimals) + + // initiate addressbook first. + addressBook = await AddressBook.new() + // setup margin pool + marginPool = await MarginPool.new(addressBook.address) + // setup margin pool v2 + borrowableMarginPool = await BorrowableMarginPool.new(addressBook.address) + // setup margin vault + const lib = await MarginVault.new() + // setup controllerProxy module + await Controller.link('MarginVault', lib.address) + controllerImplementation = await Controller.new(addressBook.address) + // setup mock Oracle module + oracle = await MockOracle.new(addressBook.address) + // setup calculator + calculator = await MarginCalculator.new(oracle.address) + // setup whitelist module + whitelist = await Whitelist.new(addressBook.address) + await whitelist.whitelistCollateral(weth.address) + await whitelist.whitelistCollateral(usdc.address) + whitelist.whitelistProduct(weth.address, usdc.address, usdc.address, true) + whitelist.whitelistProduct(weth.address, usdc.address, weth.address, false) + // setup otoken + otokenImplementation = await Otoken.new() + // setup factory + otokenFactory = await OTokenFactory.new(addressBook.address) + + // setup address book + await addressBook.setOracle(oracle.address) + await addressBook.setMarginCalculator(calculator.address) + await addressBook.setWhitelist(whitelist.address) + await addressBook.setMarginPool(marginPool.address) + await addressBook.setAddress(web3.utils.soliditySha3('BORROWABLE_POOL'), borrowableMarginPool.address, { + from: accountOwner1, + }) + await addressBook.setOtokenFactory(otokenFactory.address) + await addressBook.setOtokenImpl(otokenImplementation.address) + await addressBook.setController(controllerImplementation.address) + + const controllerProxyAddress = await addressBook.getController() + controllerProxy = await Controller.at(controllerProxyAddress) + + await otokenFactory.createOtoken( + weth.address, + usdc.address, + weth.address, + createTokenAmount(strikePrice), + expiry, + false, + ) + + const ethCallAddress = await otokenFactory.getOtoken( + weth.address, + usdc.address, + weth.address, + createTokenAmount(strikePrice), + expiry, + false, + ) + + ethCall = await Otoken.at(ethCallAddress) + // mint weth to user + const account1OwnerWeth = createTokenAmount(2 * collateralAmount, wethDecimals) + await weth.mint(accountOwner1, account1OwnerWeth) + + // have the user approve all the weth transfers + await weth.approve(borrowableMarginPool.address, account1OwnerWeth, { from: accountOwner1 }) + + const vaultCounterBefore = new BigNumber(await controllerProxy.getAccountVaultCounter(accountOwner1)) + vaultCounter = vaultCounterBefore.toNumber() + 1 + + await borrowableMarginPool.setOptionsVaultWhitelistedStatus(accountOwner1, true, { from: accountOwner1 }) + }) + + describe('Close a naked call after it expires OTM', () => { + const scaledOptionsAmount = createTokenAmount(optionsAmount, 8) + const scaledCollateralAmount = createTokenAmount(collateralAmount, wethDecimals) + const expirySpotPrice = 200 + before('Seller should be able to open a short call option', async () => { + const actionArgs = [ + { + actionType: ActionType.OpenVault, + owner: accountOwner1, + secondAddress: accountOwner1, + asset: ZERO_ADDR, + vaultId: vaultCounter, + amount: '0', + index: '0', + data: ZERO_ADDR, + }, + { + actionType: ActionType.MintShortOption, + owner: accountOwner1, + secondAddress: accountOwner1, + asset: ethCall.address, + vaultId: vaultCounter, + amount: scaledOptionsAmount, + index: '0', + data: ZERO_ADDR, + }, + { + actionType: ActionType.DepositCollateral, + owner: accountOwner1, + secondAddress: accountOwner1, + asset: weth.address, + vaultId: vaultCounter, + amount: scaledCollateralAmount, + index: '0', + data: ZERO_ADDR, + }, + ] + + await controllerProxy.operate(actionArgs, { from: accountOwner1 }) + }) + + it('Seller: close an OTM position after expiry', async () => { + // Keep track of balances before + const ownerWethBalanceBefore = new BigNumber(await weth.balanceOf(accountOwner1)) + const borrowableMarginPoolWethBalanceBefore = new BigNumber(await weth.balanceOf(borrowableMarginPool.address)) + const ownerOtokenBalanceBefore = new BigNumber(await ethCall.balanceOf(accountOwner1)) + const oTokenSupplyBefore = new BigNumber(await ethCall.totalSupply()) + + // Check that we start at a valid state + const vaultBefore = await controllerProxy.getVaultWithDetails(accountOwner1, vaultCounter) + const vaultStateBefore = await calculator.getExcessCollateral(vaultBefore[0], vaultBefore[1]) + assert.equal(vaultStateBefore[0].toString(), '0') + assert.equal(vaultStateBefore[1], true) + + // Set the oracle price + if ((await time.latest()) < expiry) { + await time.increaseTo(expiry + 2) + } + const scaledETHPrice = createTokenAmount(expirySpotPrice, 8) + const scaledUSDCPrice = createTokenAmount(1) + await oracle.setExpiryPriceFinalizedAllPeiodOver(weth.address, expiry, scaledETHPrice, true) + await oracle.setExpiryPriceFinalizedAllPeiodOver(usdc.address, expiry, scaledUSDCPrice, true) + + // Check that after expiry, the vault excess balance has updated as expected + const vaultStateBeforeSettlement = await calculator.getExcessCollateral(vaultBefore[0], vaultBefore[1]) + assert.equal(vaultStateBeforeSettlement[0].toString(), scaledCollateralAmount) + assert.equal(vaultStateBeforeSettlement[1], true) + + const actionArgs = [ + { + actionType: ActionType.SettleVault, + owner: accountOwner1, + secondAddress: accountOwner1, + asset: ZERO_ADDR, + vaultId: vaultCounter, + amount: '0', + index: '0', + data: ZERO_ADDR, + }, + ] + + await controllerProxy.operate(actionArgs, { from: accountOwner1 }) + + // keep track of balances after + const ownerWethBalanceAfter = new BigNumber(await weth.balanceOf(accountOwner1)) + const borrowableMarginPoolWethBalanceAfter = new BigNumber(await weth.balanceOf(borrowableMarginPool.address)) + + const ownerOtokenBalanceAfter = new BigNumber(await ethCall.balanceOf(accountOwner1)) + const oTokenSupplyAfter = new BigNumber(await ethCall.totalSupply()) + + // check balances before and after changed as expected + assert.equal(ownerWethBalanceBefore.plus(scaledCollateralAmount).toString(), ownerWethBalanceAfter.toString()) + assert.equal( + borrowableMarginPoolWethBalanceBefore.minus(scaledCollateralAmount).toString(), + borrowableMarginPoolWethBalanceAfter.toString(), + ) + assert.equal(ownerOtokenBalanceBefore.toString(), ownerOtokenBalanceAfter.toString()) + assert.equal(oTokenSupplyBefore.toString(), oTokenSupplyAfter.toString()) + + // Check that we end at a valid state + const vaultAfter = await controllerProxy.getVaultWithDetails(accountOwner1, vaultCounter) + const vaultStateAfter = await calculator.getExcessCollateral(vaultAfter[0], vaultBefore[1]) + assert.equal(vaultStateAfter[0].toString(), '0') + assert.equal(vaultStateAfter[1], true) + + // Check the vault balances stored in the contract + assert.equal(vaultAfter[0].shortOtokens.length, 0, 'Length of the short otoken array in the vault is incorrect') + assert.equal(vaultAfter[0].collateralAssets.length, 0, 'Length of the collateral array in the vault is incorrect') + assert.equal(vaultAfter[0].longOtokens.length, 0, 'Length of the long otoken array in the vault is incorrect') + + assert.equal(vaultAfter[0].shortAmounts.length, 0, 'Length of the short amounts array in the vault is incorrect') + assert.equal( + vaultAfter[0].collateralAmounts.length, + 0, + 'Length of the collateral amounts array in the vault is incorrect', + ) + assert.equal(vaultAfter[0].longAmounts.length, 0, 'Length of the long amounts array in the vault is incorrect') + }) + + it('Buyer: redeem OTM call option after expiry', async () => { + // owner sells their call option + await ethCall.transfer(buyer, scaledOptionsAmount, { from: accountOwner1 }) + + // Keep track of balances before + const ownerWethBalanceBefore = new BigNumber(await weth.balanceOf(buyer)) + const borrowableMarginPoolWethBalanceBefore = new BigNumber(await weth.balanceOf(borrowableMarginPool.address)) + const ownerOtokenBalanceBefore = new BigNumber(await ethCall.balanceOf(buyer)) + const oTokenSupplyBefore = new BigNumber(await ethCall.totalSupply()) + + const actionArgs = [ + { + actionType: ActionType.Redeem, + owner: buyer, + secondAddress: buyer, + asset: ethCall.address, + vaultId: '0', + amount: scaledOptionsAmount, + index: '0', + data: ZERO_ADDR, + }, + ] + + await controllerProxy.operate(actionArgs, { from: buyer }) + + // keep track of balances after + const ownerWethBalanceAfter = new BigNumber(await weth.balanceOf(buyer)) + const borrowableMarginPoolWethBalanceAfter = new BigNumber(await weth.balanceOf(borrowableMarginPool.address)) + const ownerOtokenBalanceAfter = new BigNumber(await ethCall.balanceOf(buyer)) + const oTokenSupplyAfter = new BigNumber(await ethCall.totalSupply()) + + // check balances before and after changed as expected + assert.equal(ownerWethBalanceBefore.toString(), ownerWethBalanceAfter.toString()) + assert.equal(borrowableMarginPoolWethBalanceBefore.toString(), borrowableMarginPoolWethBalanceAfter.toString()) + assert.equal(ownerOtokenBalanceBefore.minus(scaledOptionsAmount).toString(), ownerOtokenBalanceAfter.toString()) + assert.equal(oTokenSupplyBefore.minus(scaledOptionsAmount).toString(), oTokenSupplyAfter.toString()) + }) + }) +}) diff --git a/test/integration-tests/nakedCallExpireOtm.test.ts b/test/integration-tests/nakedCallExpireOtm.test.ts index eaa1b3cd6..af1e0d83a 100644 --- a/test/integration-tests/nakedCallExpireOtm.test.ts +++ b/test/integration-tests/nakedCallExpireOtm.test.ts @@ -7,6 +7,7 @@ import { ControllerInstance, WhitelistInstance, MarginPoolInstance, + BorrowableMarginPoolInstance, OtokenFactoryInstance, } from '../../build/types/truffle-types' import { createTokenAmount, createValidExpiry } from '../utils' @@ -20,6 +21,7 @@ const MockERC20 = artifacts.require('MockERC20.sol') const MarginCalculator = artifacts.require('MarginCalculator.sol') const Whitelist = artifacts.require('Whitelist.sol') const MarginPool = artifacts.require('MarginPool.sol') +const BorrowableMarginPool = artifacts.require('BorrowableMarginPool.sol') const Controller = artifacts.require('Controller.sol') const MarginVault = artifacts.require('MarginVault.sol') const OTokenFactory = artifacts.require('OtokenFactory.sol') @@ -46,6 +48,7 @@ contract('Naked Call Option expires Otm flow', ([accountOwner1, buyer]) => { let controllerProxy: ControllerInstance let controllerImplementation: ControllerInstance let marginPool: MarginPoolInstance + let borrowableMarginPool: MarginPoolInstance let whitelist: WhitelistInstance let otokenImplementation: OtokenInstance let otokenFactory: OtokenFactoryInstance @@ -78,6 +81,8 @@ contract('Naked Call Option expires Otm flow', ([accountOwner1, buyer]) => { addressBook = await AddressBook.new() // setup margin pool marginPool = await MarginPool.new(addressBook.address) + // setup margin pool v2 + borrowableMarginPool = await BorrowableMarginPool.new(addressBook.address) // setup margin vault const lib = await MarginVault.new() // setup controllerProxy module @@ -103,6 +108,9 @@ contract('Naked Call Option expires Otm flow', ([accountOwner1, buyer]) => { await addressBook.setMarginCalculator(calculator.address) await addressBook.setWhitelist(whitelist.address) await addressBook.setMarginPool(marginPool.address) + await addressBook.setAddress(web3.utils.soliditySha3('BORROWABLE_POOL'), borrowableMarginPool.address, { + from: accountOwner1, + }) await addressBook.setOtokenFactory(otokenFactory.address) await addressBook.setOtokenImpl(otokenImplementation.address) await addressBook.setController(controllerImplementation.address) diff --git a/test/integration-tests/nakedCallPreExpiry-borrowableMarginPool.test.ts b/test/integration-tests/nakedCallPreExpiry-borrowableMarginPool.test.ts new file mode 100644 index 000000000..6c41511e3 --- /dev/null +++ b/test/integration-tests/nakedCallPreExpiry-borrowableMarginPool.test.ts @@ -0,0 +1,355 @@ +import { + MockERC20Instance, + MarginCalculatorInstance, + AddressBookInstance, + MockOracleInstance, + OtokenInstance, + ControllerInstance, + WhitelistInstance, + MarginPoolInstance, + BorrowableMarginPoolInstance, + OtokenFactoryInstance, +} from '../../build/types/truffle-types' +import { createTokenAmount, createValidExpiry } from '../utils' +import BigNumber from 'bignumber.js' + +const { time } = require('@openzeppelin/test-helpers') +const AddressBook = artifacts.require('AddressBook.sol') +const MockOracle = artifacts.require('MockOracle.sol') +const Otoken = artifacts.require('Otoken.sol') +const MockERC20 = artifacts.require('MockERC20.sol') +const MarginCalculator = artifacts.require('MarginCalculator.sol') +const Whitelist = artifacts.require('Whitelist.sol') +const MarginPool = artifacts.require('MarginPool.sol') +const BorrowableMarginPool = artifacts.require('BorrowableMarginPool.sol') +const Controller = artifacts.require('Controller.sol') +const MarginVault = artifacts.require('MarginVault.sol') +const OTokenFactory = artifacts.require('OtokenFactory.sol') +const ZERO_ADDR = '0x0000000000000000000000000000000000000000' + +enum ActionType { + OpenVault, + MintShortOption, + BurnShortOption, + DepositLongOption, + WithdrawLongOption, + DepositCollateral, + WithdrawCollateral, + SettleVault, + Redeem, + Call, +} + +contract('Naked Call Option closed before expiry flow', ([accountOwner1]) => { + let expiry: number + + let addressBook: AddressBookInstance + let calculator: MarginCalculatorInstance + let controllerProxy: ControllerInstance + let controllerImplementation: ControllerInstance + let marginPool: MarginPoolInstance + let borrowableMarginPool: BorrowableMarginPoolInstance + let whitelist: WhitelistInstance + let otokenImplementation: OtokenInstance + let otokenFactory: OtokenFactoryInstance + + // oracle modulce mock + let oracle: MockOracleInstance + + let usdc: MockERC20Instance + let weth: MockERC20Instance + + let ethCall: OtokenInstance + const strikePrice = 300 + + const optionsAmount = 10 + const collateralAmount = optionsAmount + let vaultCounter: number + + const usdcDecimals = 6 + const wethDecimals = 18 + + before('set up contracts', async () => { + const now = (await time.latest()).toNumber() + expiry = createValidExpiry(now, 30) + + // setup usdc and weth + usdc = await MockERC20.new('USDC', 'USDC', usdcDecimals) + weth = await MockERC20.new('WETH', 'WETH', wethDecimals) + + // initiate addressbook first. + addressBook = await AddressBook.new() + // setup margin pool + marginPool = await MarginPool.new(addressBook.address) + // setup margin pool v2 + borrowableMarginPool = await BorrowableMarginPool.new(addressBook.address) + // setup margin vault + const lib = await MarginVault.new() + // setup controllerProxy module + await Controller.link('MarginVault', lib.address) + controllerImplementation = await Controller.new(addressBook.address) + // setup mock Oracle module + oracle = await MockOracle.new(addressBook.address) + // setup calculator + calculator = await MarginCalculator.new(oracle.address) + // setup whitelist module + whitelist = await Whitelist.new(addressBook.address) + await whitelist.whitelistCollateral(weth.address) + await whitelist.whitelistCollateral(usdc.address) + whitelist.whitelistProduct(weth.address, usdc.address, usdc.address, true) + whitelist.whitelistProduct(weth.address, usdc.address, weth.address, false) + // setup otoken + otokenImplementation = await Otoken.new() + // setup factory + otokenFactory = await OTokenFactory.new(addressBook.address) + + // setup address book + await addressBook.setOracle(oracle.address) + await addressBook.setMarginCalculator(calculator.address) + await addressBook.setWhitelist(whitelist.address) + await addressBook.setMarginPool(marginPool.address) + await addressBook.setAddress(web3.utils.soliditySha3('BORROWABLE_POOL'), borrowableMarginPool.address, { + from: accountOwner1, + }) + await addressBook.setOtokenFactory(otokenFactory.address) + await addressBook.setOtokenImpl(otokenImplementation.address) + await addressBook.setController(controllerImplementation.address) + + const controllerProxyAddress = await addressBook.getController() + controllerProxy = await Controller.at(controllerProxyAddress) + + await otokenFactory.createOtoken( + weth.address, + usdc.address, + weth.address, + createTokenAmount(strikePrice), + expiry, + false, + ) + + const ethCallAddress = await otokenFactory.getOtoken( + weth.address, + usdc.address, + weth.address, + createTokenAmount(strikePrice), + expiry, + false, + ) + + ethCall = await Otoken.at(ethCallAddress) + // mint weth to user + const account1OwnerWeth = createTokenAmount(2 * collateralAmount, wethDecimals) + await weth.mint(accountOwner1, account1OwnerWeth) + + // have the user approve all the weth transfers + await weth.approve(borrowableMarginPool.address, account1OwnerWeth, { from: accountOwner1 }) + + const vaultCounterBefore = new BigNumber(await controllerProxy.getAccountVaultCounter(accountOwner1)) + vaultCounter = vaultCounterBefore.toNumber() + 1 + + await borrowableMarginPool.setOptionsVaultWhitelistedStatus(accountOwner1, true, { from: accountOwner1 }) + }) + + describe('Integration test: Sell a naked call and close it before expiry', () => { + const scaledOptionsAmount = createTokenAmount(optionsAmount, 8) + const scaledCollateralAmount = createTokenAmount(collateralAmount, wethDecimals) + it('Seller should be able to open a short call option', async () => { + // Keep track of balances before + const ownerWethBalanceBefore = new BigNumber(await weth.balanceOf(accountOwner1)) + const borrowableMarginPoolWethBalanceBefore = new BigNumber(await weth.balanceOf(borrowableMarginPool.address)) + const ownerOtokenBalanceBefore = new BigNumber(await ethCall.balanceOf(accountOwner1)) + const oTokenSupplyBefore = new BigNumber(await ethCall.totalSupply()) + + // Check that we start at a valid state + const vaultBefore = await controllerProxy.getVaultWithDetails(accountOwner1, vaultCounter) + const vaultStateBefore = await calculator.getExcessCollateral(vaultBefore[0], vaultBefore[1]) + assert.equal(vaultStateBefore[0].toString(), '0') + assert.equal(vaultStateBefore[1], true) + + // Check the vault balances stored in the contract + assert.equal(vaultBefore[0].shortOtokens.length, 0, 'Length of the short otoken array in the vault is incorrect') + assert.equal( + vaultBefore[0].collateralAssets.length, + 0, + 'Length of the collateral array in the vault is incorrect', + ) + assert.equal(vaultBefore[0].longOtokens.length, 0, 'Length of the long otoken array in the vault is incorrect') + + assert.equal(vaultBefore[0].shortAmounts.length, 0, 'Length of the short amounts array in the vault is incorrect') + assert.equal( + vaultBefore[0].collateralAmounts.length, + 0, + 'Length of the collateral amounts array in the vault is incorrect', + ) + assert.equal(vaultBefore[0].longAmounts.length, 0, 'Length of the long amounts array in the vault is incorrect') + + const actionArgs = [ + { + actionType: ActionType.OpenVault, + owner: accountOwner1, + secondAddress: accountOwner1, + asset: ZERO_ADDR, + vaultId: vaultCounter, + amount: '0', + index: '0', + data: ZERO_ADDR, + }, + { + actionType: ActionType.MintShortOption, + owner: accountOwner1, + secondAddress: accountOwner1, + asset: ethCall.address, + vaultId: vaultCounter, + amount: scaledOptionsAmount, + index: '0', + data: ZERO_ADDR, + }, + { + actionType: ActionType.DepositCollateral, + owner: accountOwner1, + secondAddress: accountOwner1, + asset: weth.address, + vaultId: vaultCounter, + amount: scaledCollateralAmount, + index: '0', + data: ZERO_ADDR, + }, + ] + + await controllerProxy.operate(actionArgs, { from: accountOwner1 }) + + // keep track of balances after + const ownerWethBalanceAfter = new BigNumber(await weth.balanceOf(accountOwner1)) + const borrowableMarginPoolWethBalanceAfter = new BigNumber(await weth.balanceOf(borrowableMarginPool.address)) + + const ownerOtokenBalanceAfter = new BigNumber(await ethCall.balanceOf(accountOwner1)) + const oTokenSupplyAfter = new BigNumber(await ethCall.totalSupply()) + + // check balances before and after changed as expected + assert.equal(ownerWethBalanceBefore.minus(scaledCollateralAmount).toString(), ownerWethBalanceAfter.toString()) + assert.equal( + borrowableMarginPoolWethBalanceBefore.plus(scaledCollateralAmount).toString(), + borrowableMarginPoolWethBalanceAfter.toString(), + ) + assert.equal(ownerOtokenBalanceBefore.plus(scaledOptionsAmount).toString(), ownerOtokenBalanceAfter.toString()) + assert.equal(oTokenSupplyBefore.plus(scaledOptionsAmount).toString(), oTokenSupplyAfter.toString()) + + // Check that we end at a valid state + const vaultAfter = await controllerProxy.getVaultWithDetails(accountOwner1, vaultCounter) + const vaultStateAfter = await calculator.getExcessCollateral(vaultAfter[0], vaultAfter[1]) + assert.equal(vaultStateAfter[0].toString(), '0') + assert.equal(vaultStateAfter[1], true) + + // Check the vault balances stored in the contract + assert.equal(vaultAfter[0].shortOtokens.length, 1, 'Length of the short otoken array in the vault is incorrect') + assert.equal(vaultAfter[0].collateralAssets.length, 1, 'Length of the collateral array in the vault is incorrect') + assert.equal(vaultAfter[0].longOtokens.length, 0, 'Length of the long otoken array in the vault is incorrect') + + assert.equal(vaultAfter[0].shortOtokens[0], ethCall.address, 'Incorrect short otoken in the vault') + assert.equal(vaultAfter[0].collateralAssets[0], weth.address, 'Incorrect collateral asset in the vault') + + assert.equal(vaultAfter[0].shortAmounts.length, 1, 'Length of the short amounts array in the vault is incorrect') + assert.equal( + vaultAfter[0].collateralAmounts.length, + 1, + 'Length of the collateral amounts array in the vault is incorrect', + ) + assert.equal(vaultAfter[0].longAmounts.length, 0, 'Length of the long amounts array in the vault is incorrect') + + assert.equal( + vaultAfter[0].shortAmounts[0].toString(), + scaledOptionsAmount, + 'Incorrect amount of short stored in the vault', + ) + assert.equal( + vaultAfter[0].collateralAmounts[0].toString(), + scaledCollateralAmount, + 'Incorrect amount of collateral stored in the vault', + ) + }) + + it('should be able to close out the short position', async () => { + // Keep track of balances before + const ownerWethBalanceBefore = new BigNumber(await weth.balanceOf(accountOwner1)) + const borrowableMarginPoolWethBalanceBefore = new BigNumber(await weth.balanceOf(borrowableMarginPool.address)) + const ownerOtokenBalanceBefore = new BigNumber(await ethCall.balanceOf(accountOwner1)) + const oTokenSupplyBefore = new BigNumber(await ethCall.totalSupply()) + + // Check that we start at a valid state + const vaultBefore = await controllerProxy.getVaultWithDetails(accountOwner1, vaultCounter) + const vaultStateBefore = await calculator.getExcessCollateral(vaultBefore[0], vaultBefore[1]) + assert.equal(vaultStateBefore[0].toString(), '0') + assert.equal(vaultStateBefore[1], true) + + const actionArgs = [ + { + actionType: ActionType.WithdrawCollateral, + owner: accountOwner1, + secondAddress: accountOwner1, + asset: weth.address, + vaultId: vaultCounter, + amount: scaledCollateralAmount, + index: '0', + data: ZERO_ADDR, + }, + { + actionType: ActionType.BurnShortOption, + owner: accountOwner1, + secondAddress: accountOwner1, + asset: ethCall.address, + vaultId: vaultCounter, + amount: scaledOptionsAmount, + index: '0', + data: ZERO_ADDR, + }, + ] + + await controllerProxy.operate(actionArgs, { from: accountOwner1 }) + + // keep track of balances after + const ownerWethBalanceAfter = new BigNumber(await weth.balanceOf(accountOwner1)) + const borrowableMarginPoolWethBalanceAfter = new BigNumber(await weth.balanceOf(borrowableMarginPool.address)) + + const ownerOtokenBalanceAfter = new BigNumber(await ethCall.balanceOf(accountOwner1)) + const oTokenSupplyAfter = new BigNumber(await ethCall.totalSupply()) + + // check balances before and after changed as expected + assert.equal(ownerWethBalanceBefore.plus(scaledCollateralAmount).toString(), ownerWethBalanceAfter.toString()) + assert.equal( + borrowableMarginPoolWethBalanceBefore.minus(scaledCollateralAmount).toString(), + borrowableMarginPoolWethBalanceAfter.toString(), + ) + assert.equal(ownerOtokenBalanceBefore.minus(scaledOptionsAmount).toString(), ownerOtokenBalanceAfter.toString()) + assert.equal(oTokenSupplyBefore.minus(scaledOptionsAmount).toString(), oTokenSupplyAfter.toString()) + + // Check that we end at a valid state + const vaultAfter = await controllerProxy.getVaultWithDetails(accountOwner1, vaultCounter) + const vaultStateAfter = await calculator.getExcessCollateral(vaultAfter[0], vaultAfter[1]) + assert.equal(vaultStateAfter[0].toString(), '0') + assert.equal(vaultStateAfter[1], true) + + // Check the vault balances stored in the contract + assert.equal(vaultAfter[0].shortOtokens.length, 1, 'Length of the short otoken array in the vault is incorrect') + assert.equal(vaultAfter[0].collateralAssets.length, 1, 'Length of the collateral array in the vault is incorrect') + assert.equal(vaultAfter[0].longOtokens.length, 0, 'Length of the long otoken array in the vault is incorrect') + + assert.equal(vaultAfter[0].shortOtokens[0], ZERO_ADDR, 'Incorrect short otoken in the vault') + assert.equal(vaultAfter[0].collateralAssets[0], ZERO_ADDR, 'Incorrect collateral asset in the vault') + + assert.equal(vaultAfter[0].shortAmounts.length, 1, 'Length of the short amounts array in the vault is incorrect') + assert.equal( + vaultAfter[0].collateralAmounts.length, + 1, + 'Length of the collateral amounts array in the vault is incorrect', + ) + assert.equal(vaultAfter[0].longAmounts.length, 0, 'Length of the long amounts array in the vault is incorrect') + + assert.equal(vaultAfter[0].shortAmounts[0].toString(), '0', 'Incorrect amount of short stored in the vault') + assert.equal( + vaultAfter[0].collateralAmounts[0].toString(), + '0', + 'Incorrect amount of collateral stored in the vault', + ) + }) + }) +}) diff --git a/test/integration-tests/nakedCallPreExpiry.test.ts b/test/integration-tests/nakedCallPreExpiry.test.ts index dc3879b85..d5a810a05 100644 --- a/test/integration-tests/nakedCallPreExpiry.test.ts +++ b/test/integration-tests/nakedCallPreExpiry.test.ts @@ -7,6 +7,7 @@ import { ControllerInstance, WhitelistInstance, MarginPoolInstance, + BorrowableMarginPoolInstance, OtokenFactoryInstance, } from '../../build/types/truffle-types' import { createTokenAmount, createValidExpiry } from '../utils' @@ -20,6 +21,7 @@ const MockERC20 = artifacts.require('MockERC20.sol') const MarginCalculator = artifacts.require('MarginCalculator.sol') const Whitelist = artifacts.require('Whitelist.sol') const MarginPool = artifacts.require('MarginPool.sol') +const BorrowableMarginPool = artifacts.require('BorrowableMarginPool.sol') const Controller = artifacts.require('Controller.sol') const MarginVault = artifacts.require('MarginVault.sol') const OTokenFactory = artifacts.require('OtokenFactory.sol') @@ -46,6 +48,7 @@ contract('Naked Call Option closed before expiry flow', ([accountOwner1]) => { let controllerProxy: ControllerInstance let controllerImplementation: ControllerInstance let marginPool: MarginPoolInstance + let borrowableMarginPool: MarginPoolInstance let whitelist: WhitelistInstance let otokenImplementation: OtokenInstance let otokenFactory: OtokenFactoryInstance @@ -78,6 +81,8 @@ contract('Naked Call Option closed before expiry flow', ([accountOwner1]) => { addressBook = await AddressBook.new() // setup margin pool marginPool = await MarginPool.new(addressBook.address) + // setup margin pool v2 + borrowableMarginPool = await BorrowableMarginPool.new(addressBook.address) // setup margin vault const lib = await MarginVault.new() // setup controllerProxy module @@ -103,6 +108,9 @@ contract('Naked Call Option closed before expiry flow', ([accountOwner1]) => { await addressBook.setMarginCalculator(calculator.address) await addressBook.setWhitelist(whitelist.address) await addressBook.setMarginPool(marginPool.address) + await addressBook.setAddress(web3.utils.soliditySha3('BORROWABLE_POOL'), borrowableMarginPool.address, { + from: accountOwner1, + }) await addressBook.setOtokenFactory(otokenFactory.address) await addressBook.setOtokenImpl(otokenImplementation.address) await addressBook.setController(controllerImplementation.address) diff --git a/test/integration-tests/nakedMarginCallPreExpiry.test.ts b/test/integration-tests/nakedMarginCallPreExpiry.test.ts index 04ae8b21b..00c5fa672 100644 --- a/test/integration-tests/nakedMarginCallPreExpiry.test.ts +++ b/test/integration-tests/nakedMarginCallPreExpiry.test.ts @@ -7,6 +7,7 @@ import { ControllerInstance, WhitelistInstance, MarginPoolInstance, + BorrowableMarginPoolInstance, OtokenFactoryInstance, } from '../../build/types/truffle-types' import { @@ -27,6 +28,7 @@ const MockERC20 = artifacts.require('MockERC20.sol') const MarginCalculator = artifacts.require('MarginCalculator.sol') const Whitelist = artifacts.require('Whitelist.sol') const MarginPool = artifacts.require('MarginPool.sol') +const BorrowableMarginPool = artifacts.require('BorrowableMarginPool.sol') const Controller = artifacts.require('Controller.sol') const MarginVault = artifacts.require('MarginVault.sol') const OTokenFactory = artifacts.require('OtokenFactory.sol') @@ -76,6 +78,7 @@ contract('Naked margin: call position pre expiry', ([owner, accountOwner1, liqui let controllerProxy: ControllerInstance let controllerImplementation: ControllerInstance let marginPool: MarginPoolInstance + let borrowableMarginPool: MarginPoolInstance let whitelist: WhitelistInstance let otokenImplementation: OtokenInstance let otokenFactory: OtokenFactoryInstance @@ -95,6 +98,8 @@ contract('Naked margin: call position pre expiry', ([owner, accountOwner1, liqui addressBook = await AddressBook.new() // setup margin pool marginPool = await MarginPool.new(addressBook.address) + // setup margin pool v2 + borrowableMarginPool = await BorrowableMarginPool.new(addressBook.address) // setup margin vault const lib = await MarginVault.new() // setup controllerProxy module @@ -121,6 +126,9 @@ contract('Naked margin: call position pre expiry', ([owner, accountOwner1, liqui await addressBook.setMarginCalculator(calculator.address) await addressBook.setWhitelist(whitelist.address) await addressBook.setMarginPool(marginPool.address) + await addressBook.setAddress(web3.utils.soliditySha3('BORROWABLE_POOL'), borrowableMarginPool.address, { + from: owner, + }) await addressBook.setOtokenFactory(otokenFactory.address) await addressBook.setOtokenImpl(otokenImplementation.address) await addressBook.setController(controllerImplementation.address) diff --git a/test/integration-tests/nakedMarginPutPreExpiry.test.ts b/test/integration-tests/nakedMarginPutPreExpiry.test.ts index c3f4b2149..82faf806b 100644 --- a/test/integration-tests/nakedMarginPutPreExpiry.test.ts +++ b/test/integration-tests/nakedMarginPutPreExpiry.test.ts @@ -7,6 +7,7 @@ import { ControllerInstance, WhitelistInstance, MarginPoolInstance, + BorrowableMarginPoolInstance, OtokenFactoryInstance, } from '../../build/types/truffle-types' import { @@ -27,6 +28,7 @@ const MockERC20 = artifacts.require('MockERC20.sol') const MarginCalculator = artifacts.require('MarginCalculator.sol') const Whitelist = artifacts.require('Whitelist.sol') const MarginPool = artifacts.require('MarginPool.sol') +const BorrowableMarginPool = artifacts.require('BorrowableMarginPool.sol') const Controller = artifacts.require('Controller.sol') const MarginVault = artifacts.require('MarginVault.sol') const OTokenFactory = artifacts.require('OtokenFactory.sol') @@ -76,6 +78,7 @@ contract('Naked margin: put position pre expiry', ([owner, accountOwner1, buyer1 let controllerProxy: ControllerInstance let controllerImplementation: ControllerInstance let marginPool: MarginPoolInstance + let borrowableMarginPool: MarginPoolInstance let whitelist: WhitelistInstance let otokenImplementation: OtokenInstance let otokenFactory: OtokenFactoryInstance @@ -95,6 +98,8 @@ contract('Naked margin: put position pre expiry', ([owner, accountOwner1, buyer1 addressBook = await AddressBook.new() // setup margin pool marginPool = await MarginPool.new(addressBook.address) + // setup margin pool v2 + borrowableMarginPool = await BorrowableMarginPool.new(addressBook.address) // setup margin vault const lib = await MarginVault.new() // setup controllerProxy module @@ -121,6 +126,9 @@ contract('Naked margin: put position pre expiry', ([owner, accountOwner1, buyer1 await addressBook.setMarginCalculator(calculator.address) await addressBook.setWhitelist(whitelist.address) await addressBook.setMarginPool(marginPool.address) + await addressBook.setAddress(web3.utils.soliditySha3('BORROWABLE_POOL'), borrowableMarginPool.address, { + from: owner, + }) await addressBook.setOtokenFactory(otokenFactory.address) await addressBook.setOtokenImpl(otokenImplementation.address) await addressBook.setController(controllerImplementation.address) diff --git a/test/integration-tests/nakedPutExpireITM.test.ts b/test/integration-tests/nakedPutExpireITM.test.ts index 4329af730..ee2e406b6 100644 --- a/test/integration-tests/nakedPutExpireITM.test.ts +++ b/test/integration-tests/nakedPutExpireITM.test.ts @@ -7,6 +7,7 @@ import { ControllerInstance, WhitelistInstance, MarginPoolInstance, + BorrowableMarginPoolInstance, OtokenFactoryInstance, } from '../../build/types/truffle-types' import { createTokenAmount, createValidExpiry } from '../utils' @@ -20,6 +21,7 @@ const MockERC20 = artifacts.require('MockERC20.sol') const MarginCalculator = artifacts.require('MarginCalculator.sol') const Whitelist = artifacts.require('Whitelist.sol') const MarginPool = artifacts.require('MarginPool.sol') +const BorrowableMarginPool = artifacts.require('BorrowableMarginPool.sol') const Controller = artifacts.require('Controller.sol') const MarginVault = artifacts.require('MarginVault.sol') const OTokenFactory = artifacts.require('OtokenFactory.sol') @@ -46,6 +48,7 @@ contract('Naked Put Option expires Itm flow', ([accountOwner1, buyer]) => { let controllerImplementation: ControllerInstance let controllerProxy: ControllerInstance let marginPool: MarginPoolInstance + let borrowableMarginPool: MarginPoolInstance let whitelist: WhitelistInstance let otokenImplementation: OtokenInstance let otokenFactory: OtokenFactoryInstance @@ -78,6 +81,8 @@ contract('Naked Put Option expires Itm flow', ([accountOwner1, buyer]) => { addressBook = await AddressBook.new() // setup margin pool marginPool = await MarginPool.new(addressBook.address) + // setup margin pool v2 + borrowableMarginPool = await BorrowableMarginPool.new(addressBook.address) // setup margin vault const lib = await MarginVault.new() // setup controller module @@ -103,6 +108,9 @@ contract('Naked Put Option expires Itm flow', ([accountOwner1, buyer]) => { await addressBook.setMarginCalculator(calculator.address) await addressBook.setWhitelist(whitelist.address) await addressBook.setMarginPool(marginPool.address) + await addressBook.setAddress(web3.utils.soliditySha3('BORROWABLE_POOL'), borrowableMarginPool.address, { + from: accountOwner1, + }) await addressBook.setOtokenFactory(otokenFactory.address) await addressBook.setOtokenImpl(otokenImplementation.address) await addressBook.setController(controllerImplementation.address) diff --git a/test/integration-tests/nakedPutExpireOTM.test.ts b/test/integration-tests/nakedPutExpireOTM.test.ts index 4dc1cf4b4..013cbbb5c 100644 --- a/test/integration-tests/nakedPutExpireOTM.test.ts +++ b/test/integration-tests/nakedPutExpireOTM.test.ts @@ -7,6 +7,7 @@ import { ControllerInstance, WhitelistInstance, MarginPoolInstance, + BorrowableMarginPoolInstance, OtokenFactoryInstance, } from '../../build/types/truffle-types' import { createTokenAmount, createValidExpiry } from '../utils' @@ -20,6 +21,7 @@ const MockERC20 = artifacts.require('MockERC20.sol') const MarginCalculator = artifacts.require('MarginCalculator.sol') const Whitelist = artifacts.require('Whitelist.sol') const MarginPool = artifacts.require('MarginPool.sol') +const BorrowableMarginPool = artifacts.require('BorrowableMarginPool.sol') const Controller = artifacts.require('Controller.sol') const MarginVault = artifacts.require('MarginVault.sol') const OTokenFactory = artifacts.require('OtokenFactory.sol') @@ -46,6 +48,7 @@ contract('Naked Put Option expires Otm flow', ([accountOwner1, buyer]) => { let controllerImplementation: ControllerInstance let controllerProxy: ControllerInstance let marginPool: MarginPoolInstance + let borrowableMarginPool: MarginPoolInstance let whitelist: WhitelistInstance let otokenImplementation: OtokenInstance let otokenFactory: OtokenFactoryInstance @@ -78,6 +81,8 @@ contract('Naked Put Option expires Otm flow', ([accountOwner1, buyer]) => { addressBook = await AddressBook.new() // setup margin pool marginPool = await MarginPool.new(addressBook.address) + // setup margin pool v2 + borrowableMarginPool = await BorrowableMarginPool.new(addressBook.address) // setup margin vault const lib = await MarginVault.new() // setup controller module @@ -103,6 +108,9 @@ contract('Naked Put Option expires Otm flow', ([accountOwner1, buyer]) => { await addressBook.setMarginCalculator(calculator.address) await addressBook.setWhitelist(whitelist.address) await addressBook.setMarginPool(marginPool.address) + await addressBook.setAddress(web3.utils.soliditySha3('BORROWABLE_POOL'), borrowableMarginPool.address, { + from: accountOwner1, + }) await addressBook.setOtokenFactory(otokenFactory.address) await addressBook.setOtokenImpl(otokenImplementation.address) await addressBook.setController(controllerImplementation.address) diff --git a/test/integration-tests/nakedPutPreExpiry.test.ts b/test/integration-tests/nakedPutPreExpiry.test.ts index d9923637f..91a5eca9c 100644 --- a/test/integration-tests/nakedPutPreExpiry.test.ts +++ b/test/integration-tests/nakedPutPreExpiry.test.ts @@ -7,6 +7,7 @@ import { ControllerInstance, WhitelistInstance, MarginPoolInstance, + BorrowableMarginPoolInstance, OtokenFactoryInstance, } from '../../build/types/truffle-types' import { createTokenAmount, createValidExpiry } from '../utils' @@ -20,6 +21,7 @@ const MockERC20 = artifacts.require('MockERC20.sol') const MarginCalculator = artifacts.require('MarginCalculator.sol') const Whitelist = artifacts.require('Whitelist.sol') const MarginPool = artifacts.require('MarginPool.sol') +const BorrowableMarginPool = artifacts.require('BorrowableMarginPool.sol') const Controller = artifacts.require('Controller.sol') const MarginVault = artifacts.require('MarginVault.sol') const OTokenFactory = artifacts.require('OtokenFactory.sol') @@ -46,6 +48,7 @@ contract('Naked Put Option closed before expiry flow', ([accountOwner1]) => { let controllerImplementation: ControllerInstance let controllerProxy: ControllerInstance let marginPool: MarginPoolInstance + let borrowableMarginPool: MarginPoolInstance let whitelist: WhitelistInstance let otokenImplementation: OtokenInstance let otokenFactory: OtokenFactoryInstance @@ -78,6 +81,8 @@ contract('Naked Put Option closed before expiry flow', ([accountOwner1]) => { addressBook = await AddressBook.new() // setup margin pool marginPool = await MarginPool.new(addressBook.address) + // setup margin pool v2 + borrowableMarginPool = await BorrowableMarginPool.new(addressBook.address) // setup margin vault const lib = await MarginVault.new() // setup controller module @@ -103,9 +108,13 @@ contract('Naked Put Option closed before expiry flow', ([accountOwner1]) => { await addressBook.setMarginCalculator(calculator.address) await addressBook.setWhitelist(whitelist.address) await addressBook.setMarginPool(marginPool.address) + await addressBook.setAddress(web3.utils.soliditySha3('BORROWABLE_POOL'), borrowableMarginPool.address, { + from: accountOwner1, + }) await addressBook.setOtokenFactory(otokenFactory.address) await addressBook.setOtokenImpl(otokenImplementation.address) await addressBook.setController(controllerImplementation.address) + // set up controller proxy const controllerProxyAddress = await addressBook.getController() controllerProxy = await Controller.at(controllerProxyAddress) diff --git a/test/integration-tests/open-markets-borrowableMarginPool.test.ts b/test/integration-tests/open-markets-borrowableMarginPool.test.ts new file mode 100644 index 000000000..1f99a65d9 --- /dev/null +++ b/test/integration-tests/open-markets-borrowableMarginPool.test.ts @@ -0,0 +1,404 @@ +import { + OtokenFactoryInstance, + OtokenInstance, + MockOtokenInstance, + AddressBookInstance, + MockERC20Instance, + MarginPoolInstance, + BorrowableMarginPoolInstance, + ControllerInstance, + WhitelistInstance, + MockOracleInstance, +} from '../../build/types/truffle-types' + +import { assert } from 'chai' +import { createScaledNumber as createScaled, createTokenAmount, createValidExpiry } from '../utils' +const { expectRevert, time } = require('@openzeppelin/test-helpers') + +const Controller = artifacts.require('Controller.sol') +const MockERC20 = artifacts.require('MockERC20.sol') + +// real contract instances for Testing +const Otoken = artifacts.require('Otoken.sol') +const OTokenFactory = artifacts.require('OtokenFactory.sol') +const AddressBook = artifacts.require('AddressBook.sol') +const Whitelist = artifacts.require('Whitelist.sol') +const Calculator = artifacts.require('MarginCalculator.sol') +const MarginPool = artifacts.require('MarginPool.sol') +const BorrowableMarginPool = artifacts.require('BorrowableMarginPool.sol') +const MarginVault = artifacts.require('MarginVault.sol') +const MockOracle = artifacts.require('MockOracle.sol') + +// used for testing change of Otoken impl address in AddressBook +const MockOtoken = artifacts.require('MockOtoken.sol') + +const ZERO_ADDR = '0x0000000000000000000000000000000000000000' + +enum ActionType { + OpenVault, + MintShortOption, + BurnShortOption, + DepositLongOption, + WithdrawLongOption, + DepositCollateral, + WithdrawCollateral, + SettleVault, + Redeem, + Call, +} + +contract('OTokenFactory + Otoken: Cloning of real otoken instances.', ([owner, user1, user2, random]) => { + let otokenImpl: OtokenInstance + let otoken1: OtokenInstance + let otoken2: OtokenInstance + let addressBook: AddressBookInstance + let otokenFactory: OtokenFactoryInstance + let whitelist: WhitelistInstance + let marginPool: MarginPoolInstance + let borrowableMarginPool: BorrowableMarginPoolInstance + let oracle: MockOracleInstance + + let usdc: MockERC20Instance + let dai: MockERC20Instance + let weth: MockERC20Instance + let randomERC20: MockERC20Instance + + let controller: ControllerInstance + + const strikePrice = createTokenAmount(200) + const isPut = true + let expiry: number + + before('Deploy addressBook, otoken logic, whitelist, Factory contract', async () => { + expiry = createValidExpiry(Number(await time.latest()), 100) + usdc = await MockERC20.new('USDC', 'USDC', 6) + dai = await MockERC20.new('DAI', 'DAI', 18) + weth = await MockERC20.new('wETH', 'WETH', 18) + randomERC20 = await MockERC20.new('RANDOM', 'RAM', 10) + + // Setup AddresBook + addressBook = await AddressBook.new({ from: owner }) + + oracle = await MockOracle.new(addressBook.address, { from: owner }) + otokenImpl = await Otoken.new({ from: owner }) + whitelist = await Whitelist.new(addressBook.address, { from: owner }) + otokenFactory = await OTokenFactory.new(addressBook.address, { from: owner }) + marginPool = await MarginPool.new(addressBook.address) + borrowableMarginPool = await BorrowableMarginPool.new(addressBook.address) + + const calculator = await Calculator.new(addressBook.address, { from: owner }) + + // setup addressBook + await addressBook.setOtokenImpl(otokenImpl.address, { from: owner }) + await addressBook.setWhitelist(whitelist.address, { from: owner }) + await addressBook.setOtokenFactory(otokenFactory.address, { from: owner }) + await addressBook.setMarginCalculator(calculator.address, { from: owner }) + await addressBook.setMarginPool(marginPool.address, { from: owner }) + await addressBook.setAddress(web3.utils.soliditySha3('BORROWABLE_POOL'), borrowableMarginPool.address, { + from: owner, + }) + await addressBook.setOracle(oracle.address, { from: owner }) + + // deploy the controller instance + const lib = await MarginVault.new() + await Controller.link('MarginVault', lib.address) + controller = await Controller.new() + await controller.initialize(addressBook.address, owner) + + // set the controller as controller (so it has access to minting tokens) + await addressBook.setController(controller.address) + controller = await Controller.at(await addressBook.getController()) + + await borrowableMarginPool.setOptionsVaultWhitelistedStatus(user1, true, { from: owner }) + }) + + describe('Otoken Creation before whitelisting', () => { + it('Should revert before admin whitelist any product', async () => { + await expectRevert( + otokenFactory.createOtoken(weth.address, usdc.address, usdc.address, strikePrice, expiry, isPut, { + from: user1, + }), + 'OtokenFactory: Unsupported Product', + ) + }) + }) + + describe('Otoken Creation after whitelisting products and collateral', () => { + before('Whitelist product from admin', async () => { + await whitelist.whitelistCollateral(usdc.address, { from: owner }) + await whitelist.whitelistCollateral(dai.address, { from: owner }) + await whitelist.whitelistProduct(weth.address, usdc.address, usdc.address, isPut, { from: owner }) + await whitelist.whitelistProduct(weth.address, dai.address, dai.address, isPut, { from: owner }) + }) + + it('Should init otoken1 with correct name and symbol', async () => { + // otoken1: eth-usdc option + const targetAddress1 = await otokenFactory.getTargetOtokenAddress( + weth.address, + usdc.address, + usdc.address, + strikePrice, + expiry, + isPut, + ) + await otokenFactory.createOtoken(weth.address, usdc.address, usdc.address, strikePrice, expiry, isPut, { + from: user1, + }) + otoken1 = await Otoken.at(targetAddress1) + assert.isTrue((await otoken1.name()).includes('200Put USDC Collateral')) + assert.isTrue((await otoken1.symbol()).includes('oWETHUSDC/USDC-')) + }) + + it('Should init otoken2 with correct name and symbol', async () => { + // otoken2: eth-dai option + const targetAddress2 = await otokenFactory.getTargetOtokenAddress( + weth.address, + dai.address, + dai.address, + strikePrice, + expiry, + isPut, + ) + await otokenFactory.createOtoken(weth.address, dai.address, dai.address, strikePrice, expiry, isPut, { + from: user2, + }) + otoken2 = await Otoken.at(targetAddress2) + assert.isTrue((await otoken2.name()).includes('200Put DAI Collateral')) + assert.isTrue((await otoken2.symbol()).includes('oWETHDAI')) + }) + + it('The newly created tokens should be whitelisted in the whitelist module', async () => { + assert.equal(await whitelist.isWhitelistedOtoken(otoken1.address), true) + assert.equal(await whitelist.isWhitelistedOtoken(otoken2.address), true) + }) + + it('The owner of whitelist contract can blacklist specific otoken', async () => { + await whitelist.blacklistOtoken(otoken2.address) + + assert.equal(await whitelist.isWhitelistedOtoken(otoken2.address), false) + }) + + it('should revert creating otoken after the owner blacklist the product', async () => { + await whitelist.blacklistProduct(weth.address, usdc.address, usdc.address, isPut) + const newStrikePrice = createScaled(188) + await expectRevert( + otokenFactory.createOtoken(weth.address, usdc.address, usdc.address, newStrikePrice, expiry, isPut, { + from: user1, + }), + 'OtokenFactory: Unsupported Product', + ) + }) + + it('Should revert when calling init after createOtoken', async () => { + /* Should revert because the init function is already called by the factory in createOtoken() */ + await expectRevert( + otoken1.init(addressBook.address, usdc.address, usdc.address, usdc.address, strikePrice, expiry, isPut), + 'Contract instance has already been initialized', + ) + + await expectRevert( + otoken2.init(addressBook.address, randomERC20.address, dai.address, usdc.address, strikePrice, expiry, isPut), + 'Contract instance has already been initialized', + ) + }) + + it('Calling init on otoken logic contract should not affecting the minimal proxies.', async () => { + // someone call init on the logic function for no reason + await otokenImpl.init( + addressBook.address, + randomERC20.address, + randomERC20.address, + weth.address, + strikePrice, + expiry, + isPut, + ) + + const strikeOfOtoken1 = await otoken1.strikeAsset() + const collateralOfOtoken1 = await otoken1.collateralAsset() + + const strikeOfOtoken2 = await otoken2.strikeAsset() + const collateralOfOtoken2 = await otoken2.collateralAsset() + + assert.equal(strikeOfOtoken1, usdc.address) + assert.equal(strikeOfOtoken2, dai.address) + assert.equal(collateralOfOtoken1, usdc.address) + assert.equal(collateralOfOtoken2, dai.address) + }) + }) + + describe('Controller only functions on cloned otokens', () => { + const amountToMint = createTokenAmount(10) + + it('should revert when mintOtoken is called by random address', async () => { + await expectRevert( + otoken1.mintOtoken(user1, amountToMint, { from: random }), + 'Otoken: Only Controller can mint Otokens', + ) + }) + + it('should be able to mint token1 from controller', async () => { + // the controller will call otoken1.mintOtoken() + await whitelist.whitelistCollateral(usdc.address, { from: owner }) + const vaultCounter = 1 + const amountCollateral = createTokenAmount(2000, 6) + await usdc.mint(user1, amountCollateral) + await usdc.approve(borrowableMarginPool.address, amountCollateral, { from: user1 }) + const actionArgs = [ + { + actionType: ActionType.OpenVault, + owner: user1, + secondAddress: user1, + asset: ZERO_ADDR, + vaultId: vaultCounter, + amount: '0', + index: '0', + data: ZERO_ADDR, + }, + { + actionType: ActionType.MintShortOption, + owner: user1, + secondAddress: user1, + asset: otoken1.address, + vaultId: vaultCounter, + amount: amountToMint, + index: '0', + data: ZERO_ADDR, + }, + { + actionType: ActionType.DepositCollateral, + owner: user1, + secondAddress: user1, + asset: usdc.address, + vaultId: vaultCounter, + amount: amountCollateral, + index: '0', + data: ZERO_ADDR, + }, + ] + await controller.operate(actionArgs, { from: user1 }) + // await controller.testMintOtoken(otoken1.address, user1, amountToMint.toString()) + const balance = await otoken1.balanceOf(user1) + assert.equal(balance.toString(), amountToMint.toString()) + }) + + it('should revert when burnOtoken is called by random address', async () => { + await expectRevert( + otoken1.burnOtoken(user1, amountToMint, { from: random }), + 'Otoken: Only Controller can burn Otokens', + ) + }) + + it('should be able to burn tokens from controller', async () => { + const vaultCounter = 1 + const amountCollateral = createTokenAmount(2000, 6) + await otoken1.approve(borrowableMarginPool.address, amountToMint, { from: user1 }) + + const actionArgs = [ + { + actionType: ActionType.BurnShortOption, + owner: user1, + secondAddress: user1, + asset: otoken1.address, + vaultId: vaultCounter, + amount: amountToMint, + index: '0', + data: ZERO_ADDR, + }, + { + actionType: ActionType.WithdrawCollateral, + owner: user1, + secondAddress: user1, + asset: usdc.address, + vaultId: vaultCounter, + amount: amountCollateral, + index: '0', + data: ZERO_ADDR, + }, + ] + await controller.operate(actionArgs, { from: user1 }) + const balance = await otoken1.balanceOf(user1) + assert.equal(balance.toString(), '0') + }) + }) + + describe('Otoken Implementation address upgrade ', () => { + const amountToMint = createScaled(10) + + before('whitelist product again', async () => { + await whitelist.whitelistProduct(weth.address, usdc.address, usdc.address, isPut, { from: owner }) + }) + + it('should not affect existing otoken instances', async () => { + const newOtoken = await MockOtoken.new() + // mint some otoken1 + const vaultCounter = 1 + const amountCollateral = createTokenAmount(2000, 6) + await usdc.mint(user1, amountCollateral) + await usdc.approve(borrowableMarginPool.address, amountCollateral, { from: user1 }) + const actionArgs = [ + { + actionType: ActionType.MintShortOption, + owner: user1, + secondAddress: user1, + asset: otoken1.address, + vaultId: vaultCounter, + amount: amountToMint, + index: '0', + data: ZERO_ADDR, + }, + { + actionType: ActionType.DepositCollateral, + owner: user1, + secondAddress: user1, + asset: usdc.address, + vaultId: vaultCounter, + amount: amountCollateral, + index: '0', + data: ZERO_ADDR, + }, + ] + await controller.operate(actionArgs, { from: user1 }) + + // update otokenimpl address in addressbook + await addressBook.setOtokenImpl(newOtoken.address, { from: owner }) + + const balance = await otoken1.balanceOf(user1) + assert.equal(balance.toString(), amountToMint.toString()) + }) + + it('should deploy MockOtoken after upgrade', async () => { + const newExpiry = expiry + 86400 + const address = await otokenFactory.getTargetOtokenAddress( + weth.address, + usdc.address, + usdc.address, + strikePrice, + newExpiry, + isPut, + ) + await otokenFactory.createOtoken(weth.address, usdc.address, usdc.address, strikePrice, newExpiry, isPut) + + const mockedToken: MockOtokenInstance = await MockOtoken.at(address) + // Only MockOtoken has this method, if it return true that means we created a MockOtoken instance. + const inited = await mockedToken.inited() + assert.isTrue(inited) + }) + }) + + describe('Market creation after addressbook update', () => { + before('update the factory address in the address book', async () => { + await addressBook.setOtokenFactory(random) + }) + + it('should revert when trying to create oToken', async () => { + const newStrikePrice = createScaled(400) + await expectRevert( + otokenFactory.createOtoken(weth.address, usdc.address, usdc.address, newStrikePrice, expiry, isPut, { + from: user1, + }), + 'Whitelist: Sender is not OtokenFactory', + ) + }) + }) +}) diff --git a/test/integration-tests/open-markets.test.ts b/test/integration-tests/open-markets.test.ts index f551244c4..40c2e7ac6 100644 --- a/test/integration-tests/open-markets.test.ts +++ b/test/integration-tests/open-markets.test.ts @@ -5,6 +5,7 @@ import { AddressBookInstance, MockERC20Instance, MarginPoolInstance, + BorrowableMarginPoolInstance, ControllerInstance, WhitelistInstance, MockOracleInstance, @@ -24,6 +25,7 @@ const AddressBook = artifacts.require('AddressBook.sol') const Whitelist = artifacts.require('Whitelist.sol') const Calculator = artifacts.require('MarginCalculator.sol') const MarginPool = artifacts.require('MarginPool.sol') +const BorrowableMarginPool = artifacts.require('BorrowableMarginPool.sol') const MarginVault = artifacts.require('MarginVault.sol') const MockOracle = artifacts.require('MockOracle.sol') @@ -53,6 +55,7 @@ contract('OTokenFactory + Otoken: Cloning of real otoken instances.', ([owner, u let otokenFactory: OtokenFactoryInstance let whitelist: WhitelistInstance let marginPool: MarginPoolInstance + let borrowableMarginPool: MarginPoolInstance let oracle: MockOracleInstance let usdc: MockERC20Instance @@ -81,6 +84,8 @@ contract('OTokenFactory + Otoken: Cloning of real otoken instances.', ([owner, u whitelist = await Whitelist.new(addressBook.address, { from: owner }) otokenFactory = await OTokenFactory.new(addressBook.address, { from: owner }) marginPool = await MarginPool.new(addressBook.address) + borrowableMarginPool = await BorrowableMarginPool.new(addressBook.address) + const calculator = await Calculator.new(addressBook.address, { from: owner }) // setup addressBook @@ -89,6 +94,9 @@ contract('OTokenFactory + Otoken: Cloning of real otoken instances.', ([owner, u await addressBook.setOtokenFactory(otokenFactory.address, { from: owner }) await addressBook.setMarginCalculator(calculator.address, { from: owner }) await addressBook.setMarginPool(marginPool.address, { from: owner }) + await addressBook.setAddress(web3.utils.soliditySha3('BORROWABLE_POOL'), borrowableMarginPool.address, { + from: owner, + }) await addressBook.setOracle(oracle.address, { from: owner }) // deploy the controller instance diff --git a/test/integration-tests/rollover.test.ts b/test/integration-tests/rollover.test.ts index a3facdd6b..c14767c80 100644 --- a/test/integration-tests/rollover.test.ts +++ b/test/integration-tests/rollover.test.ts @@ -7,6 +7,7 @@ import { ControllerInstance, WhitelistInstance, MarginPoolInstance, + BorrowableMarginPoolInstance, OtokenFactoryInstance, } from '../../build/types/truffle-types' import { createTokenAmount, createValidExpiry } from '../utils' @@ -20,6 +21,7 @@ const MockERC20 = artifacts.require('MockERC20.sol') const MarginCalculator = artifacts.require('MarginCalculator.sol') const Whitelist = artifacts.require('Whitelist.sol') const MarginPool = artifacts.require('MarginPool.sol') +const BorrowableMarginPool = artifacts.require('BorrowableMarginPool.sol') const Controller = artifacts.require('Controller.sol') const MarginVault = artifacts.require('MarginVault.sol') const OTokenFactory = artifacts.require('OtokenFactory.sol') @@ -47,6 +49,7 @@ contract('Rollover Naked Put Option flow', ([accountOwner1, accountOperator1, bu let controllerImplementation: ControllerInstance let controllerProxy: ControllerInstance let marginPool: MarginPoolInstance + let borrowableMarginPool: MarginPoolInstance let whitelist: WhitelistInstance let otokenImplementation: OtokenInstance let otokenFactory: OtokenFactoryInstance @@ -82,6 +85,8 @@ contract('Rollover Naked Put Option flow', ([accountOwner1, accountOperator1, bu addressBook = await AddressBook.new() // setup margin pool marginPool = await MarginPool.new(addressBook.address) + // setup margin pool v2 + borrowableMarginPool = await BorrowableMarginPool.new(addressBook.address) // setup margin vault const lib = await MarginVault.new() // setup controller module @@ -107,6 +112,9 @@ contract('Rollover Naked Put Option flow', ([accountOwner1, accountOperator1, bu await addressBook.setMarginCalculator(calculator.address) await addressBook.setWhitelist(whitelist.address) await addressBook.setMarginPool(marginPool.address) + await addressBook.setAddress(web3.utils.soliditySha3('BORROWABLE_POOL'), borrowableMarginPool.address, { + from: accountOwner1, + }) await addressBook.setOtokenFactory(otokenFactory.address) await addressBook.setOtokenImpl(otokenImplementation.address) await addressBook.setController(controllerImplementation.address) diff --git a/test/integration-tests/shortCallSpreadExpireItm.test.ts b/test/integration-tests/shortCallSpreadExpireItm.test.ts index 18d5c444f..3a353951b 100644 --- a/test/integration-tests/shortCallSpreadExpireItm.test.ts +++ b/test/integration-tests/shortCallSpreadExpireItm.test.ts @@ -7,6 +7,7 @@ import { ControllerInstance, WhitelistInstance, MarginPoolInstance, + BorrowableMarginPoolInstance, OtokenFactoryInstance, } from '../../build/types/truffle-types' import { createTokenAmount, createValidExpiry } from '../utils' @@ -20,6 +21,7 @@ const MockERC20 = artifacts.require('MockERC20.sol') const MarginCalculator = artifacts.require('MarginCalculator.sol') const Whitelist = artifacts.require('Whitelist.sol') const MarginPool = artifacts.require('MarginPool.sol') +const BorrowableMarginPool = artifacts.require('BorrowableMarginPool.sol') const Controller = artifacts.require('Controller.sol') const MarginVault = artifacts.require('MarginVault.sol') const OTokenFactory = artifacts.require('OtokenFactory.sol') @@ -46,6 +48,7 @@ contract('Short Call Spread Option expires Itm flow', ([accountOwner1, nakedBuye let controllerProxy: ControllerInstance let controllerImplementation: ControllerInstance let marginPool: MarginPoolInstance + let borrowableMarginPool: MarginPoolInstance let whitelist: WhitelistInstance let otokenImplementation: OtokenInstance let otokenFactory: OtokenFactoryInstance @@ -82,6 +85,8 @@ contract('Short Call Spread Option expires Itm flow', ([accountOwner1, nakedBuye addressBook = await AddressBook.new() // setup margin pool marginPool = await MarginPool.new(addressBook.address) + // setup margin pool v2 + borrowableMarginPool = await BorrowableMarginPool.new(addressBook.address) // setup margin vault const lib = await MarginVault.new() // setup controllerProxy module @@ -107,6 +112,9 @@ contract('Short Call Spread Option expires Itm flow', ([accountOwner1, nakedBuye await addressBook.setMarginCalculator(calculator.address) await addressBook.setWhitelist(whitelist.address) await addressBook.setMarginPool(marginPool.address) + await addressBook.setAddress(web3.utils.soliditySha3('BORROWABLE_POOL'), borrowableMarginPool.address, { + from: accountOwner1, + }) await addressBook.setOtokenFactory(otokenFactory.address) await addressBook.setOtokenImpl(otokenImplementation.address) await addressBook.setController(controllerImplementation.address) diff --git a/test/integration-tests/shortCallSpreadExpireOtm.test.ts b/test/integration-tests/shortCallSpreadExpireOtm.test.ts index bacbf5c23..fe868eb0b 100644 --- a/test/integration-tests/shortCallSpreadExpireOtm.test.ts +++ b/test/integration-tests/shortCallSpreadExpireOtm.test.ts @@ -7,6 +7,7 @@ import { ControllerInstance, WhitelistInstance, MarginPoolInstance, + BorrowableMarginPoolInstance, OtokenFactoryInstance, } from '../../build/types/truffle-types' import { createTokenAmount, createValidExpiry } from '../utils' @@ -20,6 +21,7 @@ const MockERC20 = artifacts.require('MockERC20.sol') const MarginCalculator = artifacts.require('MarginCalculator.sol') const Whitelist = artifacts.require('Whitelist.sol') const MarginPool = artifacts.require('MarginPool.sol') +const BorrowableMarginPool = artifacts.require('BorrowableMarginPool.sol') const Controller = artifacts.require('Controller.sol') const MarginVault = artifacts.require('MarginVault.sol') const OTokenFactory = artifacts.require('OtokenFactory.sol') @@ -46,6 +48,7 @@ contract('Short Call Spread Option expires Otm flow', ([accountOwner1, nakedBuye let controllerProxy: ControllerInstance let controllerImplementation: ControllerInstance let marginPool: MarginPoolInstance + let borrowableMarginPool: MarginPoolInstance let whitelist: WhitelistInstance let otokenImplementation: OtokenInstance let otokenFactory: OtokenFactoryInstance @@ -81,6 +84,8 @@ contract('Short Call Spread Option expires Otm flow', ([accountOwner1, nakedBuye addressBook = await AddressBook.new() // setup margin pool marginPool = await MarginPool.new(addressBook.address) + // setup margin pool v2 + borrowableMarginPool = await BorrowableMarginPool.new(addressBook.address) // setup margin vault const lib = await MarginVault.new() // setup controllerProxy module @@ -106,6 +111,9 @@ contract('Short Call Spread Option expires Otm flow', ([accountOwner1, nakedBuye await addressBook.setMarginCalculator(calculator.address) await addressBook.setWhitelist(whitelist.address) await addressBook.setMarginPool(marginPool.address) + await addressBook.setAddress(web3.utils.soliditySha3('BORROWABLE_POOL'), borrowableMarginPool.address, { + from: accountOwner1, + }) await addressBook.setOtokenFactory(otokenFactory.address) await addressBook.setOtokenImpl(otokenImplementation.address) await addressBook.setController(controllerImplementation.address) diff --git a/test/integration-tests/shortCallSpreadPreExpiry.test.ts b/test/integration-tests/shortCallSpreadPreExpiry.test.ts index eb08c7e6e..5c493052f 100644 --- a/test/integration-tests/shortCallSpreadPreExpiry.test.ts +++ b/test/integration-tests/shortCallSpreadPreExpiry.test.ts @@ -7,6 +7,7 @@ import { ControllerInstance, WhitelistInstance, MarginPoolInstance, + BorrowableMarginPoolInstance, OtokenFactoryInstance, } from '../../build/types/truffle-types' import { createTokenAmount, createValidExpiry } from '../utils' @@ -20,6 +21,7 @@ const MockERC20 = artifacts.require('MockERC20.sol') const MarginCalculator = artifacts.require('MarginCalculator.sol') const Whitelist = artifacts.require('Whitelist.sol') const MarginPool = artifacts.require('MarginPool.sol') +const BorrowableMarginPool = artifacts.require('BorrowableMarginPool.sol') const Controller = artifacts.require('Controller.sol') const MarginVault = artifacts.require('MarginVault.sol') const OTokenFactory = artifacts.require('OtokenFactory.sol') @@ -46,6 +48,7 @@ contract('Short Call Spread Option closed before expiry flow', ([accountOwner1, let controllerProxy: ControllerInstance let controllerImplementation: ControllerInstance let marginPool: MarginPoolInstance + let borrowableMarginPool: MarginPoolInstance let whitelist: WhitelistInstance let otokenImplementation: OtokenInstance let otokenFactory: OtokenFactoryInstance @@ -82,6 +85,8 @@ contract('Short Call Spread Option closed before expiry flow', ([accountOwner1, addressBook = await AddressBook.new() // setup margin pool marginPool = await MarginPool.new(addressBook.address) + // setup margin pool v2 + borrowableMarginPool = await BorrowableMarginPool.new(addressBook.address) // setup margin vault const lib = await MarginVault.new() // setup controllerProxy module @@ -107,6 +112,9 @@ contract('Short Call Spread Option closed before expiry flow', ([accountOwner1, await addressBook.setMarginCalculator(calculator.address) await addressBook.setWhitelist(whitelist.address) await addressBook.setMarginPool(marginPool.address) + await addressBook.setAddress(web3.utils.soliditySha3('BORROWABLE_POOL'), borrowableMarginPool.address, { + from: accountOwner1, + }) await addressBook.setOtokenFactory(otokenFactory.address) await addressBook.setOtokenImpl(otokenImplementation.address) await addressBook.setController(controllerImplementation.address) diff --git a/test/integration-tests/shortPutSpreadExpireItm.test.ts b/test/integration-tests/shortPutSpreadExpireItm.test.ts index 1c597f650..7f5ed46bd 100644 --- a/test/integration-tests/shortPutSpreadExpireItm.test.ts +++ b/test/integration-tests/shortPutSpreadExpireItm.test.ts @@ -7,6 +7,7 @@ import { ControllerInstance, WhitelistInstance, MarginPoolInstance, + BorrowableMarginPoolInstance, OtokenFactoryInstance, } from '../../build/types/truffle-types' import { createTokenAmount, createValidExpiry } from '../utils' @@ -20,6 +21,7 @@ const MockERC20 = artifacts.require('MockERC20.sol') const MarginCalculator = artifacts.require('MarginCalculator.sol') const Whitelist = artifacts.require('Whitelist.sol') const MarginPool = artifacts.require('MarginPool.sol') +const BorrowableMarginPool = artifacts.require('BorrowableMarginPool.sol') const Controller = artifacts.require('Controller.sol') const MarginVault = artifacts.require('MarginVault.sol') const OTokenFactory = artifacts.require('OtokenFactory.sol') @@ -46,6 +48,7 @@ contract('Short Put Spread Option expires Itm flow', ([accountOwner1, nakedBuyer let controllerProxy: ControllerInstance let controllerImplementation: ControllerInstance let marginPool: MarginPoolInstance + let borrowableMarginPool: MarginPoolInstance let whitelist: WhitelistInstance let otokenImplementation: OtokenInstance let otokenFactory: OtokenFactoryInstance @@ -82,6 +85,8 @@ contract('Short Put Spread Option expires Itm flow', ([accountOwner1, nakedBuyer addressBook = await AddressBook.new() // setup margin pool marginPool = await MarginPool.new(addressBook.address) + // setup margin pool v2 + borrowableMarginPool = await BorrowableMarginPool.new(addressBook.address) // setup margin vault const lib = await MarginVault.new() // setup controllerProxy module @@ -107,6 +112,9 @@ contract('Short Put Spread Option expires Itm flow', ([accountOwner1, nakedBuyer await addressBook.setMarginCalculator(calculator.address) await addressBook.setWhitelist(whitelist.address) await addressBook.setMarginPool(marginPool.address) + await addressBook.setAddress(web3.utils.soliditySha3('BORROWABLE_POOL'), borrowableMarginPool.address, { + from: accountOwner1, + }) await addressBook.setOtokenFactory(otokenFactory.address) await addressBook.setOtokenImpl(otokenImplementation.address) await addressBook.setController(controllerImplementation.address) diff --git a/test/integration-tests/shortPutSpreadExpireOtm.test.ts b/test/integration-tests/shortPutSpreadExpireOtm.test.ts index da7414e27..0fcd642f5 100644 --- a/test/integration-tests/shortPutSpreadExpireOtm.test.ts +++ b/test/integration-tests/shortPutSpreadExpireOtm.test.ts @@ -7,6 +7,7 @@ import { ControllerInstance, WhitelistInstance, MarginPoolInstance, + BorrowableMarginPoolInstance, OtokenFactoryInstance, } from '../../build/types/truffle-types' import { createTokenAmount, createValidExpiry } from '../utils' @@ -20,6 +21,7 @@ const MockERC20 = artifacts.require('MockERC20.sol') const MarginCalculator = artifacts.require('MarginCalculator.sol') const Whitelist = artifacts.require('Whitelist.sol') const MarginPool = artifacts.require('MarginPool.sol') +const BorrowableMarginPool = artifacts.require('BorrowableMarginPool.sol') const Controller = artifacts.require('Controller.sol') const MarginVault = artifacts.require('MarginVault.sol') const OTokenFactory = artifacts.require('OtokenFactory.sol') @@ -46,6 +48,7 @@ contract('Short Put Spread Option expires Otm flow', ([accountOwner1, nakedBuyer let controllerProxy: ControllerInstance let controllerImplementation: ControllerInstance let marginPool: MarginPoolInstance + let borrowableMarginPool: MarginPoolInstance let whitelist: WhitelistInstance let otokenImplementation: OtokenInstance let otokenFactory: OtokenFactoryInstance @@ -82,6 +85,8 @@ contract('Short Put Spread Option expires Otm flow', ([accountOwner1, nakedBuyer addressBook = await AddressBook.new() // setup margin pool marginPool = await MarginPool.new(addressBook.address) + // setup margin pool v2 + borrowableMarginPool = await BorrowableMarginPool.new(addressBook.address) // setup margin vault const lib = await MarginVault.new() // setup controllerProxy module @@ -107,6 +112,9 @@ contract('Short Put Spread Option expires Otm flow', ([accountOwner1, nakedBuyer await addressBook.setMarginCalculator(calculator.address) await addressBook.setWhitelist(whitelist.address) await addressBook.setMarginPool(marginPool.address) + await addressBook.setAddress(web3.utils.soliditySha3('BORROWABLE_POOL'), borrowableMarginPool.address, { + from: accountOwner1, + }) await addressBook.setOtokenFactory(otokenFactory.address) await addressBook.setOtokenImpl(otokenImplementation.address) await addressBook.setController(controllerImplementation.address) diff --git a/test/integration-tests/shortPutSpreadPreExpiry.test.ts b/test/integration-tests/shortPutSpreadPreExpiry.test.ts index fb57e6f7c..2c8d5a405 100644 --- a/test/integration-tests/shortPutSpreadPreExpiry.test.ts +++ b/test/integration-tests/shortPutSpreadPreExpiry.test.ts @@ -7,6 +7,7 @@ import { ControllerInstance, WhitelistInstance, MarginPoolInstance, + BorrowableMarginPoolInstance, OtokenFactoryInstance, } from '../../build/types/truffle-types' import { createTokenAmount, createValidExpiry } from '../utils' @@ -20,6 +21,7 @@ const MockERC20 = artifacts.require('MockERC20.sol') const MarginCalculator = artifacts.require('MarginCalculator.sol') const Whitelist = artifacts.require('Whitelist.sol') const MarginPool = artifacts.require('MarginPool.sol') +const BorrowableMarginPool = artifacts.require('BorrowableMarginPool.sol') const Controller = artifacts.require('Controller.sol') const MarginVault = artifacts.require('MarginVault.sol') const OTokenFactory = artifacts.require('OtokenFactory.sol') @@ -46,6 +48,7 @@ contract('Short Put Spread Option closed before expiry flow', ([accountOwner1, n let controllerProxy: ControllerInstance let controllerImplementation: ControllerInstance let marginPool: MarginPoolInstance + let borrowableMarginPool: MarginPoolInstance let whitelist: WhitelistInstance let otokenImplementation: OtokenInstance let otokenFactory: OtokenFactoryInstance @@ -82,6 +85,8 @@ contract('Short Put Spread Option closed before expiry flow', ([accountOwner1, n addressBook = await AddressBook.new() // setup margin pool marginPool = await MarginPool.new(addressBook.address) + // setup margin pool v2 + borrowableMarginPool = await BorrowableMarginPool.new(addressBook.address) // setup margin vault const lib = await MarginVault.new() // setup controllerProxy module @@ -107,6 +112,9 @@ contract('Short Put Spread Option closed before expiry flow', ([accountOwner1, n await addressBook.setMarginCalculator(calculator.address) await addressBook.setWhitelist(whitelist.address) await addressBook.setMarginPool(marginPool.address) + await addressBook.setAddress(web3.utils.soliditySha3('BORROWABLE_POOL'), borrowableMarginPool.address, { + from: accountOwner1, + }) await addressBook.setOtokenFactory(otokenFactory.address) await addressBook.setOtokenImpl(otokenImplementation.address) await addressBook.setController(controllerImplementation.address) diff --git a/test/integration-tests/yieldFarming.test.ts b/test/integration-tests/yieldFarming.test.ts index a444e498e..741b4c56c 100644 --- a/test/integration-tests/yieldFarming.test.ts +++ b/test/integration-tests/yieldFarming.test.ts @@ -7,6 +7,7 @@ import { ControllerInstance, WhitelistInstance, MarginPoolInstance, + BorrowableMarginPoolInstance, OtokenFactoryInstance, MockPricerInstance, MockCTokenInstance, @@ -23,6 +24,7 @@ const MockERC20 = artifacts.require('MockERC20.sol') const MarginCalculator = artifacts.require('MarginCalculator.sol') const Whitelist = artifacts.require('Whitelist.sol') const MarginPool = artifacts.require('MarginPool.sol') +const BorrowableMarginPool = artifacts.require('BorrowableMarginPool.sol') const Controller = artifacts.require('Controller.sol') const MarginVault = artifacts.require('MarginVault.sol') const OTokenFactory = artifacts.require('OtokenFactory.sol') @@ -52,6 +54,7 @@ contract('Yield Farming: Naked Put Option closed before expiry flow', ([admin, a let controllerImplementation: ControllerInstance let controllerProxy: ControllerInstance let marginPool: MarginPoolInstance + let borrowableMarginPool: MarginPoolInstance let whitelist: WhitelistInstance let otokenImplementation: OtokenInstance let otokenFactory: OtokenFactoryInstance @@ -84,6 +87,8 @@ contract('Yield Farming: Naked Put Option closed before expiry flow', ([admin, a addressBook = await AddressBook.new() // setup margin pool marginPool = await MarginPool.new(addressBook.address) + // setup margin pool v2 + borrowableMarginPool = await BorrowableMarginPool.new(addressBook.address) // setup margin account const lib = await MarginVault.new() // setup controller module @@ -109,6 +114,9 @@ contract('Yield Farming: Naked Put Option closed before expiry flow', ([admin, a await addressBook.setMarginCalculator(calculator.address) await addressBook.setWhitelist(whitelist.address) await addressBook.setMarginPool(marginPool.address) + await addressBook.setAddress(web3.utils.soliditySha3('BORROWABLE_POOL'), borrowableMarginPool.address, { + from: admin, + }) await addressBook.setOtokenFactory(otokenFactory.address) await addressBook.setOtokenImpl(otokenImplementation.address) await addressBook.setController(controllerImplementation.address) diff --git a/test/unit-tests/borrowableMarginPool.test.ts b/test/unit-tests/borrowableMarginPool.test.ts new file mode 100644 index 000000000..d9ded34a1 --- /dev/null +++ b/test/unit-tests/borrowableMarginPool.test.ts @@ -0,0 +1,1087 @@ +import { + MockERC20Instance, + MockAddressBookInstance, + MockWhitelistModuleInstance, + WETH9Instance, + MarginPoolInstance, + BorrowableMarginPoolInstance, + MockDumbERC20Instance, + OtokenInstance, +} from '../../build/types/truffle-types' +import { createTokenAmount } from '../utils' + +import BigNumber from 'bignumber.js' + +const { expectRevert, ether } = require('@openzeppelin/test-helpers') + +const MockERC20 = artifacts.require('MockERC20.sol') +const MockDumbERC20 = artifacts.require('MockDumbERC20.sol') +const MockAddressBook = artifacts.require('MockAddressBook.sol') +const MockWhitelist = artifacts.require('MockWhitelistModule.sol') +const WETH9 = artifacts.require('WETH9.sol') +const MarginPool = artifacts.require('BorrowableMarginPool.sol') +const Otoken = artifacts.require('Otoken.sol') + +// address(0) +const ZERO_ADDR = '0x0000000000000000000000000000000000000000' + +contract('MarginPool', ([owner, controllerAddress, farmer, user1, random]) => { + const usdcToMint = ether('100000') + const wethToMint = ether('50') + const otokenAmount = createTokenAmount(10) + + const strikePrice = createTokenAmount(200) + const expiry = 1916380800 // 2030/09/25 0800 UTC + const isPut = false + + const TOTAL_PCT = 10000 + + // ERC20 mocks + let usdc: MockERC20Instance + let weth: WETH9Instance + // DumbER20: Return false when transfer fail. + let dumbToken: MockDumbERC20Instance + // addressbook module mock + let addressBook: MockAddressBookInstance + // whitelist module mock + let whitelist: MockWhitelistModuleInstance + // margin pool + let marginPool: BorrowableMarginPoolInstance + // mock oToken + let otoken: OtokenInstance + + before('Deployment', async () => { + // deploy USDC token + usdc = await MockERC20.new('USDC', 'USDC', 18) + // deploy WETH token for testing + weth = await WETH9.new() + // deploy dumb erc20 + dumbToken = await MockDumbERC20.new('DUSDC', 'DUSDC', 18) + // deploy AddressBook mock + addressBook = await MockAddressBook.new() + // deploy whitelist mock + whitelist = await MockWhitelist.new() + // set Controller module address + await addressBook.setController(controllerAddress) + // set Whitelist module address + await addressBook.setWhitelist(whitelist.address) + + // deploy MarginPool module + marginPool = await MarginPool.new(addressBook.address) + + otoken = await Otoken.new() + + await otoken.init(addressBook.address, weth.address, usdc.address, usdc.address, strikePrice, expiry, isPut, { + from: owner, + }) + + // mint usdc + await usdc.mint(user1, usdcToMint) + // mint usdc + await usdc.mint(controllerAddress, usdcToMint) + // wrap ETH in Controller module level + await weth.deposit({ from: controllerAddress, value: wethToMint }) + + // controller approving infinite amount of WETH to pool + await weth.approve(marginPool.address, wethToMint, { from: controllerAddress }) + // controller approving infinite amount of USDC to pool + await usdc.approve(marginPool.address, usdcToMint, { from: controllerAddress }) + + // transfer to pool + await marginPool.transferToPool(weth.address, controllerAddress, ether('25'), { from: controllerAddress }) + // transfer to pool + await marginPool.transferToPool(usdc.address, controllerAddress, usdcToMint, { from: controllerAddress }) + }) + + describe('MarginPool initialization', () => { + it('should revert if initilized with 0 addressBook address', async () => { + await expectRevert(MarginPool.new(ZERO_ADDR), 'Invalid address book') + }) + }) + + describe('Transfer to pool', () => { + const usdcToTransfer = ether('250') + const wethToTransfer = ether('10') + + it('should revert transfering to pool from caller other than controller address', async () => { + // user approve USDC transfer + await usdc.approve(marginPool.address, usdcToTransfer, { from: user1 }) + + await expectRevert( + marginPool.transferToPool(usdc.address, user1, usdcToTransfer, { from: random }), + 'MarginPool: Sender is not Controller', + ) + }) + + it('should revert transfering to pool an amount equal to zero', async () => { + // user approve USDC transfer + await usdc.approve(marginPool.address, usdcToTransfer, { from: user1 }) + + await expectRevert( + marginPool.transferToPool(usdc.address, user1, ether('0'), { from: controllerAddress }), + 'MarginPool: transferToPool amount is equal to 0', + ) + }) + + it('should revert transfering to pool if the address of the sender is the margin pool', async () => { + // user approve USDC transfer + await usdc.approve(marginPool.address, usdcToTransfer, { from: user1 }) + + await expectRevert( + marginPool.transferToPool(usdc.address, marginPool.address, new BigNumber(usdcToMint).plus(1), { + from: controllerAddress, + }), + 'ERC20: transfer amount exceeds balance', + ) + }) + + it('should transfer to pool from user when called by the controller address', async () => { + const userBalanceBefore = new BigNumber(await usdc.balanceOf(user1)) + const poolBalanceBefore = new BigNumber(await usdc.balanceOf(marginPool.address)) + + // user approve USDC transfer + await usdc.approve(marginPool.address, usdcToTransfer, { from: user1 }) + + await marginPool.transferToPool(usdc.address, user1, usdcToTransfer, { from: controllerAddress }) + + const userBalanceAfter = new BigNumber(await usdc.balanceOf(user1)) + const poolBalanceAfter = new BigNumber(await usdc.balanceOf(marginPool.address)) + + assert.equal( + new BigNumber(usdcToTransfer).toString(), + userBalanceBefore.minus(userBalanceAfter).toString(), + 'USDC value transfered from user mismatch', + ) + + assert.equal( + new BigNumber(usdcToTransfer).toString(), + poolBalanceAfter.minus(poolBalanceBefore).toString(), + 'USDC value transfered into pool mismatch', + ) + }) + + it('should transfer WETH to pool from controller when called by the controller address', async () => { + const controllerBalanceBefore = new BigNumber(await weth.balanceOf(controllerAddress)) + const poolBalanceBefore = new BigNumber(await weth.balanceOf(marginPool.address)) + + await marginPool.transferToPool(weth.address, controllerAddress, wethToTransfer, { from: controllerAddress }) + + const controllerBalanceAfter = new BigNumber(await weth.balanceOf(controllerAddress)) + const poolBalanceAfter = new BigNumber(await weth.balanceOf(marginPool.address)) + + assert.equal( + new BigNumber(wethToTransfer).toString(), + controllerBalanceBefore.minus(controllerBalanceAfter).toString(), + 'WETH value transfered from controller mismatch', + ) + + assert.equal( + new BigNumber(wethToTransfer).toString(), + poolBalanceAfter.minus(poolBalanceBefore).toString(), + 'WETH value transfered into pool mismatch', + ) + }) + + it('should revert when transferFrom return false on dumbERC20', async () => { + await expectRevert( + marginPool.transferToPool(dumbToken.address, user1, ether('1'), { from: controllerAddress }), + 'SafeERC20: ERC20 operation did not succeed', + ) + }) + }) + + describe('Transfer to user', () => { + const usdcToTransfer = ether('250') + const wethToTransfer = ether('10') + + it('should revert transfering to user from caller other than controller address', async () => { + await expectRevert( + marginPool.transferToUser(usdc.address, user1, usdcToTransfer, { from: random }), + 'MarginPool: Sender is not Controller', + ) + }) + + it('should revert transfering to user if the user address is the margin pool addres', async () => { + await expectRevert( + marginPool.transferToUser(usdc.address, marginPool.address, usdcToTransfer, { from: controllerAddress }), + 'MarginPool: cannot transfer assets to oneself', + ) + }) + + it('should transfer an ERC-20 to user from pool when called by the controller address', async () => { + const userBalanceBefore = new BigNumber(await usdc.balanceOf(user1)) + const poolBalanceBefore = new BigNumber(await usdc.balanceOf(marginPool.address)) + + await marginPool.transferToUser(usdc.address, user1, usdcToTransfer, { from: controllerAddress }) + + const userBalanceAfter = new BigNumber(await usdc.balanceOf(user1)) + const poolBalanceAfter = new BigNumber(await usdc.balanceOf(marginPool.address)) + + assert.equal( + new BigNumber(usdcToTransfer).toString(), + userBalanceAfter.minus(userBalanceBefore).toString(), + 'USDC value transfered to user mismatch', + ) + + assert.equal( + new BigNumber(usdcToTransfer).toString(), + poolBalanceBefore.minus(poolBalanceAfter).toString(), + 'USDC value transfered from pool mismatch', + ) + }) + + it('should transfer WETH to controller from pool, unwrap it and transfer ETH to user when called by the controller address', async () => { + const poolBalanceBefore = new BigNumber(await weth.balanceOf(marginPool.address)) + const userBalanceBefore = new BigNumber(await web3.eth.getBalance(user1)) + + // transfer to controller + await marginPool.transferToUser(weth.address, controllerAddress, wethToTransfer, { from: controllerAddress }) + // unwrap WETH to ETH + await weth.withdraw(wethToTransfer, { from: controllerAddress }) + // send ETH to user + await web3.eth.sendTransaction({ from: controllerAddress, to: user1, value: wethToTransfer }) + + const poolBalanceAfter = new BigNumber(await weth.balanceOf(marginPool.address)) + const userBalanceAfter = new BigNumber(await web3.eth.getBalance(user1)) + + assert.equal( + new BigNumber(wethToTransfer).toString(), + poolBalanceBefore.minus(poolBalanceAfter).toString(), + 'WETH value un-wrapped from pool mismatch', + ) + + assert.equal( + new BigNumber(wethToTransfer).toString(), + userBalanceAfter.minus(userBalanceBefore).toString(), + 'ETH value transfered to user mismatch', + ) + }) + + it('should revert when transfer return false on dumbERC20', async () => { + await dumbToken.mint(user1, ether('1')) + await dumbToken.approve(marginPool.address, ether('1'), { from: user1 }) + await marginPool.transferToPool(dumbToken.address, user1, ether('1'), { from: controllerAddress }) + // let the transfer failed + await dumbToken.setLocked(true) + await expectRevert( + marginPool.transferToUser(dumbToken.address, user1, ether('1'), { from: controllerAddress }), + 'SafeERC20: ERC20 operation did not succeed', + ) + await dumbToken.setLocked(false) + }) + }) + + describe('Transfer multiple assets to pool', () => { + const usdcToTransfer = ether('250') + const wethToTransfer = ether('10') + + it('should revert transfering an array to pool from caller other than controller address', async () => { + // user approve USDC and WETH transfer + await usdc.approve(marginPool.address, usdcToTransfer, { from: user1 }) + await weth.approve(marginPool.address, wethToTransfer, { from: user1 }) + + await expectRevert( + marginPool.batchTransferToPool([usdc.address, weth.address], [user1, user1], [usdcToTransfer, wethToTransfer], { + from: random, + }), + 'MarginPool: Sender is not Controller', + ) + }) + it('should revert transfering to pool an array with an amount equal to zero', async () => { + // user approve USDC transfer + await usdc.approve(marginPool.address, usdcToTransfer, { from: user1 }) + await weth.approve(marginPool.address, wethToTransfer, { from: user1 }) + + await expectRevert( + marginPool.batchTransferToPool([usdc.address, weth.address], [user1, user1], [ether('0'), wethToTransfer], { + from: controllerAddress, + }), + 'MarginPool: transferToPool amount is equal to 0', + ) + }) + + it('should revert with different size arrays', async () => { + await expectRevert( + marginPool.batchTransferToPool( + [usdc.address, weth.address], + [user1, user1], + [usdcToTransfer, usdcToTransfer, usdcToTransfer], + { from: controllerAddress }, + ), + 'MarginPool: batchTransferToPool array lengths are not equal', + ) + }) + + it('should transfer an array including weth and usdc to pool from user/controller when called by the controller address', async () => { + const userUsdcBalanceBefore = new BigNumber(await usdc.balanceOf(user1)) + const poolUsdcBalanceBefore = new BigNumber(await usdc.balanceOf(marginPool.address)) + const controllerWethBalanceBefore = new BigNumber(await weth.balanceOf(controllerAddress)) + const poolWethBalanceBefore = new BigNumber(await weth.balanceOf(marginPool.address)) + + // user approve USDC and WETH transfer + await usdc.approve(marginPool.address, usdcToTransfer, { from: user1 }) + await weth.approve(marginPool.address, wethToTransfer, { from: user1 }) + + await marginPool.batchTransferToPool( + [usdc.address, weth.address], + [user1, controllerAddress], + [usdcToTransfer, wethToTransfer], + { from: controllerAddress }, + ) + + const userUsdcBalanceAfter = new BigNumber(await usdc.balanceOf(user1)) + const poolUsdcBalanceAfter = new BigNumber(await usdc.balanceOf(marginPool.address)) + const controllerWethBalanceAfter = new BigNumber(await weth.balanceOf(controllerAddress)) + const poolWethBalanceAfter = new BigNumber(await weth.balanceOf(marginPool.address)) + + assert.equal( + new BigNumber(usdcToTransfer).toString(), + userUsdcBalanceBefore.minus(userUsdcBalanceAfter).toString(), + 'USDC value transfered from user mismatch', + ) + + assert.equal( + new BigNumber(usdcToTransfer).toString(), + poolUsdcBalanceAfter.minus(poolUsdcBalanceBefore).toString(), + 'USDC value transfered into pool mismatch', + ) + + assert.equal( + new BigNumber(wethToTransfer).toString(), + controllerWethBalanceBefore.minus(controllerWethBalanceAfter).toString(), + 'WETH value transfered from controller mismatch', + ) + + assert.equal( + new BigNumber(wethToTransfer).toString(), + poolWethBalanceAfter.minus(poolWethBalanceBefore).toString(), + 'WETH value transfered into pool mismatch', + ) + }) + }) + + describe('Transfer multiple assets to user', () => { + const usdcToTransfer = ether('250') + const wethToTransfer = ether('25') + + it('should revert transfering to user from caller other than controller address', async () => { + await expectRevert( + marginPool.batchTransferToUser([usdc.address, weth.address], [user1, user1], [usdcToTransfer, wethToTransfer], { + from: random, + }), + 'MarginPool: Sender is not Controller', + ) + }) + + it('should revert with different size arrays', async () => { + await expectRevert( + marginPool.batchTransferToUser( + [usdc.address, weth.address], + [user1, user1], + [usdcToTransfer, usdcToTransfer, usdcToTransfer], + { from: controllerAddress }, + ), + 'MarginPool: batchTransferToUser array lengths are not equal', + ) + }) + + it('should batch transfer to users when called from controller', async () => { + const userUsdcBalanceBefore = new BigNumber(await usdc.balanceOf(user1)) + const poolUsdcBalanceBefore = new BigNumber(await usdc.balanceOf(marginPool.address)) + const controllerWethBalanceBefore = new BigNumber(await weth.balanceOf(controllerAddress)) + const poolWethBalanceBefore = new BigNumber(await weth.balanceOf(marginPool.address)) + + await marginPool.batchTransferToUser( + [usdc.address, weth.address], + [user1, controllerAddress], + [usdcToTransfer, poolWethBalanceBefore], + { from: controllerAddress }, + ) + + const userUsdcBalanceAfter = new BigNumber(await usdc.balanceOf(user1)) + const poolUsdcBalanceAfter = new BigNumber(await usdc.balanceOf(marginPool.address)) + const controllerWethBalanceAfter = new BigNumber(await weth.balanceOf(controllerAddress)) + const poolWethBalanceAfter = new BigNumber(await weth.balanceOf(marginPool.address)) + + assert.equal( + usdcToTransfer.toString(), + userUsdcBalanceAfter.minus(userUsdcBalanceBefore).toString(), + 'USDC value transfered to user mismatch', + ) + + assert.equal( + usdcToTransfer.toString(), + poolUsdcBalanceBefore.minus(poolUsdcBalanceAfter).toString(), + 'USDC value transfered from pool mismatch', + ) + + assert.equal( + poolWethBalanceBefore.toString(), + controllerWethBalanceAfter.minus(controllerWethBalanceBefore).toString(), + 'WETH value transfered to controller mismatch', + ) + + assert.equal( + poolWethBalanceBefore.toString(), + poolWethBalanceBefore.minus(poolWethBalanceAfter).toString(), + 'WETH value transfered from pool mismatch', + ) + }) + }) + + describe('Farming', () => { + before(async () => { + // send more usdc to pool + await usdc.mint(marginPool.address, new BigNumber('100')) + }) + + it('should revert setting farmer address from non-owner', async () => { + await expectRevert(marginPool.setFarmer(farmer, { from: random }), 'Ownable: caller is not the owner') + }) + + it('should set farmer address when called from owner', async () => { + await marginPool.setFarmer(farmer, { from: owner }) + + assert.equal(await marginPool.farmer(), farmer, 'farmer address mismatch') + }) + + it('should revert farming when receiver address is equal to zero', async () => { + const poolStoredBalanceBefore = new BigNumber(await marginPool.getStoredBalance(usdc.address)) + const poolBlanaceBefore = new BigNumber(await usdc.balanceOf(marginPool.address)) + const amountToFarm = poolBlanaceBefore.minus(poolStoredBalanceBefore) + + await expectRevert( + marginPool.farm(usdc.address, ZERO_ADDR, amountToFarm, { from: farmer }), + 'MarginPool: invalid receiver address', + ) + }) + + it('should revert farming when sender is not farmer address', async () => { + const poolStoredBalanceBefore = new BigNumber(await marginPool.getStoredBalance(usdc.address)) + const poolBlanaceBefore = new BigNumber(await usdc.balanceOf(marginPool.address)) + const amountToFarm = poolBlanaceBefore.minus(poolStoredBalanceBefore) + + await expectRevert( + marginPool.farm(usdc.address, random, amountToFarm, { from: random }), + 'MarginPool: Sender is not farmer', + ) + }) + + it('should farm additional USDC', async () => { + const poolStoredBalanceBefore = new BigNumber(await marginPool.getStoredBalance(usdc.address)) + const poolBlanaceBefore = new BigNumber(await usdc.balanceOf(marginPool.address)) + const farmerBalanceBefore = new BigNumber(await usdc.balanceOf(farmer)) + const amountToFarm = poolBlanaceBefore.minus(poolStoredBalanceBefore) + + await marginPool.farm(usdc.address, farmer, amountToFarm, { from: farmer }) + + const poolStoredBalanceAfter = new BigNumber(await marginPool.getStoredBalance(usdc.address)) + const poolBlanaceAfter = new BigNumber(await usdc.balanceOf(marginPool.address)) + const farmerBalanceAfter = new BigNumber(await usdc.balanceOf(farmer)) + + assert.equal( + poolStoredBalanceBefore.toString(), + poolStoredBalanceAfter.toString(), + 'Pool stored balance mismatch', + ) + assert.equal( + poolBlanaceBefore.minus(poolBlanaceAfter).toString(), + amountToFarm.toString(), + 'Pool balance mismatch', + ) + assert.equal( + farmerBalanceAfter.minus(farmerBalanceBefore).toString(), + amountToFarm.toString(), + 'Farmer balance mismatch', + ) + }) + + it('should revert farming when amount is greater than available balance to farm', async () => { + const amountToFarm = new BigNumber('100000000000') + + await expectRevert( + marginPool.farm(usdc.address, farmer, amountToFarm, { from: farmer }), + 'MarginPool: amount to farm exceeds limit', + ) + }) + + it('should revert farming when transfer return false for dumbERC20', async () => { + const amountExcess = ether('1') + await dumbToken.mint(marginPool.address, amountExcess) + await dumbToken.setLocked(true) + + await expectRevert( + marginPool.farm(dumbToken.address, farmer, amountExcess, { from: farmer }), + 'SafeERC20: ERC20 operation did not succeed', + ) + }) + }) + + describe('Set Borrow Whitelist', () => { + it('should revert calling setBorrowerWhitelistedStatus from non-owner', async () => { + await expectRevert( + marginPool.setBorrowerWhitelistedStatus(random, true, { from: random }), + 'Ownable: caller is not the owner', + ) + }) + + it('should revert calling setBorrowerWhitelistedStatus on zero address', async () => { + await expectRevert( + marginPool.setBorrowerWhitelistedStatus(ZERO_ADDR, true, { from: owner }), + 'MarginPool: Invalid Borrower', + ) + }) + + it('should set whitelist status to true when called from owner', async () => { + await marginPool.setBorrowerWhitelistedStatus(random, true, { from: owner }) + + assert.equal(await marginPool.isWhitelistedBorrower(random), true, 'whitelist status mismatch') + }) + + it('should set whitelist status to false when called from owner', async () => { + await marginPool.setBorrowerWhitelistedStatus(random, true, { from: owner }) + + assert.equal(await marginPool.isWhitelistedBorrower(random), true, 'whitelist status mismatch') + + await marginPool.setBorrowerWhitelistedStatus(random, false, { from: owner }) + + assert.equal(await marginPool.isWhitelistedBorrower(random), false, 'whitelist status mismatch') + }) + }) + + describe('Set oToken Buyer Whitelist', () => { + it('should revert calling setOTokenBuyerWhitelistedStatus from non-owner', async () => { + await expectRevert( + marginPool.setOTokenBuyerWhitelistedStatus(random, true, { from: random }), + 'Ownable: caller is not the owner', + ) + }) + + it('should revert calling setOTokenBuyerWhitelistedStatus on zero address', async () => { + await expectRevert( + marginPool.setOTokenBuyerWhitelistedStatus(ZERO_ADDR, true, { from: owner }), + 'MarginPool: Invalid oToken Buyer', + ) + }) + + it('should set whitelist status to true when called from owner', async () => { + await marginPool.setOTokenBuyerWhitelistedStatus(random, true, { from: owner }) + + assert.equal(await marginPool.isWhitelistedOTokenBuyer(random), true, 'whitelist status mismatch') + }) + + it('should set whitelist status to false when called from owner', async () => { + await marginPool.setOTokenBuyerWhitelistedStatus(random, true, { from: owner }) + + assert.equal(await marginPool.isWhitelistedOTokenBuyer(random), true, 'whitelist status mismatch') + + await marginPool.setOTokenBuyerWhitelistedStatus(random, false, { from: owner }) + + assert.equal(await marginPool.isWhitelistedOTokenBuyer(random), false, 'whitelist status mismatch') + }) + }) + + describe('Set Options Vault Whitelist', () => { + it('should revert calling setOptionsVaultWhitelistedStatus from non-owner', async () => { + await expectRevert( + marginPool.setOptionsVaultWhitelistedStatus(random, true, { from: random }), + 'Ownable: caller is not the owner', + ) + }) + + it('should revert calling setOptionsVaultWhitelistedStatus on zero address', async () => { + await expectRevert( + marginPool.setOptionsVaultWhitelistedStatus(ZERO_ADDR, true, { from: owner }), + 'MarginPool: Invalid Options Vault', + ) + }) + + it('should revert calling setOptionsVaultWhitelistedStatus on retail vault', async () => { + await marginPool.setOptionsVaultToRetailStatus([farmer]) + + await expectRevert( + marginPool.setOptionsVaultWhitelistedStatus(farmer, true, { from: owner }), + 'MarginPool: Cannot whitelist a retail vault', + ) + }) + + it('should set whitelist status to true when called from owner', async () => { + await marginPool.setOptionsVaultWhitelistedStatus(random, true, { from: owner }) + + assert.equal(await marginPool.isWhitelistedOptionsVault(random), true, 'whitelist status mismatch') + }) + + it('should set whitelist status to false when called from owner', async () => { + await marginPool.setOptionsVaultWhitelistedStatus(random, true, { from: owner }) + + assert.equal(await marginPool.isWhitelistedOptionsVault(random), true, 'whitelist status mismatch') + + await marginPool.setOptionsVaultWhitelistedStatus(random, false, { from: owner }) + + assert.equal(await marginPool.isWhitelistedOptionsVault(random), false, 'whitelist status mismatch') + }) + }) + + describe('Set Options Vault to Retail', () => { + it('should revert calling setIsRetailVault from non-owner', async () => { + await expectRevert( + marginPool.setOptionsVaultToRetailStatus([random], { from: random }), + 'Ownable: caller is not the owner', + ) + }) + + it('should set option vault retail vault status to true when called from owner', async () => { + await marginPool.setOptionsVaultToRetailStatus([random], { from: owner }) + + assert.equal(await marginPool.isRetailOptionsVault(random), true, 'whitelist status mismatch') + }) + + it('should set multiple option vaults retail vault status to true when called from owner', async () => { + await marginPool.setOptionsVaultToRetailStatus([random, ZERO_ADDR, owner, controllerAddress], { from: owner }) + + assert.equal(await marginPool.isRetailOptionsVault(random), true, 'whitelist status mismatch') + assert.equal(await marginPool.isRetailOptionsVault(owner), true, 'whitelist status mismatch') + assert.equal(await marginPool.isRetailOptionsVault(controllerAddress), true, 'whitelist status mismatch') + }) + }) + + describe('Set Borrow Percent', () => { + it('should revert calling setBorrowPCT from non-owner', async () => { + await expectRevert( + marginPool.setBorrowPCT(await otoken.collateralAsset(), 1, { from: random }), + 'Ownable: caller is not the owner', + ) + }) + + it('should set borrow percent when called from owner', async () => { + await marginPool.setBorrowPCT(await otoken.collateralAsset(), 100, { from: owner }) + + assert.equal( + new BigNumber(await marginPool.borrowPCT(await otoken.collateralAsset())).toString(), + '100', + 'borrowability status mismatch', + ) + }) + }) + + describe('Borrow', () => { + beforeEach(async () => { + otoken = await Otoken.new() + + await otoken.init(addressBook.address, weth.address, usdc.address, usdc.address, strikePrice, expiry, isPut, { + from: owner, + }) + + await marginPool.setBorrowerWhitelistedStatus(random, true, { from: owner }) + await marginPool.setBorrowerWhitelistedStatus(user1, true, { from: owner }) + await otoken.mintOtoken(user1, otokenAmount, { from: controllerAddress }) + await marginPool.setBorrowPCT(await otoken.collateralAsset(), TOTAL_PCT, { from: owner }) + await whitelist.whitelistOtoken(otoken.address) + }) + + it('should revert borrowing if not whitelisted borrower', async () => { + await marginPool.setBorrowerWhitelistedStatus(random, false, { from: owner }) + + assert.equal( + (await marginPool.borrowable(random, otoken.address)).toString(), + new BigNumber('0').toString(), + 'Borrowable amount mismatch', + ) + + await expectRevert( + marginPool.borrow(otoken.address, 1, { from: random }), + 'MarginPool: Sender is not whitelisted borrower', + ) + }) + + it('should revert borrowing if borrowing 0 of underlying', async () => { + await expectRevert( + marginPool.borrow(otoken.address, 0, { from: random }), + 'MarginPool: Cannot borrow 0 of underlying', + ) + }) + + it('should revert borrowing if borrowPCT = 0', async () => { + await marginPool.setBorrowPCT(await otoken.collateralAsset(), 0, { from: owner }) + + assert.equal( + (await marginPool.borrowable(random, otoken.address)).toString(), + new BigNumber('0').toString(), + 'Borrowable amount mismatch', + ) + + await expectRevert( + marginPool.borrow(otoken.address, 1, { from: random }), + 'MarginPool: Borrowing more than allocated', + ) + }) + + it('should revert borrowing if using expired oToken', async () => { + const otoken2 = await Otoken.new() + + const strikePrice = createTokenAmount(200) + const isPut = false + + await otoken2.init( + addressBook.address, + weth.address, + usdc.address, + usdc.address, + strikePrice, + 1637792800, + isPut, + { + from: owner, + }, + ) + + // check otoken whitelist + await whitelist.whitelistOtoken(otoken2.address) + + await expectRevert( + marginPool.borrow(otoken2.address, 1, { from: random }), + 'MarginPool: Cannot borrow collateral asset of expired oToken', + ) + }) + + it('should revert borrowing if using put oToken', async () => { + const otoken2 = await Otoken.new() + + const strikePrice = createTokenAmount(200) + const isPut = true + + await otoken2.init( + addressBook.address, + weth.address, + usdc.address, + usdc.address, + strikePrice, + 1637792800, + isPut, + { + from: owner, + }, + ) + + // check otoken whitelist + await whitelist.whitelistOtoken(otoken2.address) + + await expectRevert( + marginPool.borrow(otoken2.address, 1, { from: random }), + 'MarginPool: oToken is not a call option', + ) + }) + + it('should revert borrowing if using blacklisted oToken', async () => { + const otoken2 = await Otoken.new() + + const strikePrice = createTokenAmount(200) + const isPut = false + + await otoken2.init( + addressBook.address, + weth.address, + usdc.address, + usdc.address, + strikePrice, + 1637792800, + isPut, + { + from: owner, + }, + ) + + await expectRevert( + marginPool.borrow(otoken2.address, 1, { from: random }), + 'MarginPool: oToken is not whitelisted', + ) + }) + + it('should revert borrowing if borrowing more than allocated', async () => { + await expectRevert( + marginPool.borrow(otoken.address, 1, { from: random }), + 'MarginPool: Borrowing more than allocated', + ) + + await expectRevert( + marginPool.borrow(otoken.address, new BigNumber(await otoken.balanceOf(user1)).plus(1), { from: user1 }), + 'MarginPool: Borrowing more than allocated', + ) + }) + + it('should revert borrowing if borrowing more than allocated (with borrowPCT chance)', async () => { + const borrowPCT = 5000 + + await marginPool.setBorrowPCT(await otoken.collateralAsset(), borrowPCT, { from: owner }) + + const oTokenAmount = new BigNumber(await otoken.balanceOf(user1)) + const oTokenToUnderlying = oTokenAmount.times(new BigNumber(10).exponentiatedBy(10)) + + assert.equal( + oTokenToUnderlying.div(2).toString(), + new BigNumber(await marginPool.borrowable(user1, otoken.address)).toString(), + 'Borrowable amount mismatch', + ) + + await expectRevert( + marginPool.borrow(otoken.address, oTokenAmount.div(2).plus(1), { from: user1 }), + 'MarginPool: Borrowing more than allocated', + ) + }) + + it('should increase borrow capacity after increasing otoken balance', async () => { + const oTokenBalanceBefore = await otoken.balanceOf(user1) + const amtBorrowableBefore = await marginPool.borrowable(user1, otoken.address) + + await otoken.mintOtoken(user1, otokenAmount, { from: controllerAddress }) + + const amtBorrowableAfter = await marginPool.borrowable(user1, otoken.address) + + const oTokenToUnderlying = new BigNumber( + new BigNumber(await otoken.balanceOf(user1)).minus(oTokenBalanceBefore), + ).times(new BigNumber(10).exponentiatedBy(10)) + + assert.equal( + oTokenToUnderlying.toString(), + new BigNumber(amtBorrowableAfter).minus(amtBorrowableBefore).toString(), + 'Borrowable amount mismatch', + ) + }) + + it('should transfer collateral asset to borrower', async () => { + const oTokenBalanceBefore = await otoken.balanceOf(user1) + const borrowerBalanceBefore = new BigNumber(await usdc.balanceOf(user1)) + const amtBorrowableBefore = await marginPool.borrowable(user1, otoken.address) + + const oTokenAmount = new BigNumber(await otoken.balanceOf(user1)) + const oTokenToUnderlying = oTokenAmount.times(new BigNumber(10).exponentiatedBy(10)) + + await otoken.approve(marginPool.address, oTokenAmount, { from: user1 }) + await marginPool.borrow(otoken.address, oTokenAmount, { from: user1 }) + + const borrowerBalanceAfter = new BigNumber(await usdc.balanceOf(user1)) + const amtBorrowableAfter = await marginPool.borrowable(user1, otoken.address) + const oTokenBalanceAfter = await otoken.balanceOf(user1) + + await usdc.approve(marginPool.address, oTokenToUnderlying, { from: user1 }) + await marginPool.repay(otoken.address, oTokenToUnderlying, { from: user1 }) + + assert.equal( + new BigNumber('0').toString(), + new BigNumber(oTokenBalanceAfter).toString(), + 'oToken amount mismatch', + ) + + assert.equal( + oTokenToUnderlying.toString(), + new BigNumber(amtBorrowableBefore).minus(amtBorrowableAfter).toString(), + 'Borrowable amount mismatch', + ) + + assert.equal( + new BigNumber('0').toString(), + new BigNumber(amtBorrowableAfter).toString(), + 'oToken amount mismatch', + ) + + assert.equal( + oTokenToUnderlying.toString(), + new BigNumber(borrowerBalanceAfter).minus(borrowerBalanceBefore).toString(), + 'WETH value transfered from margin pool mismatch', + ) + }) + + it('should transfer collateral asset to borrower after setting borrow PCT', async () => { + const oTokenBalanceBefore = await otoken.balanceOf(user1) + + const oTokenAmount = new BigNumber(await otoken.balanceOf(user1)) + const oTokenToUnderlying = oTokenAmount.times(new BigNumber(10).exponentiatedBy(10)) + + await otoken.approve(marginPool.address, oTokenAmount, { from: user1 }) + + await marginPool.borrow(otoken.address, oTokenAmount.div(4), { from: user1 }) + + // 50% borrowable + await marginPool.setBorrowPCT(await otoken.collateralAsset(), 5000, { from: owner }) + + await marginPool.borrow(otoken.address, oTokenAmount.div(4), { from: user1 }) + + const oTokenBalanceAfter = await otoken.balanceOf(user1) + + assert.equal( + new BigNumber(await marginPool.borrowable(user1, otoken.address)).toString(), + new BigNumber('0').toString(), + 'Borrowable amount mismatch', + ) + + assert.equal( + new BigNumber(oTokenBalanceBefore).div(2).toString(), + new BigNumber(oTokenBalanceBefore).minus(oTokenBalanceAfter).toString(), + 'oToken amount mismatch', + ) + + await usdc.approve(marginPool.address, oTokenToUnderlying, { from: user1 }) + await marginPool.repay(otoken.address, oTokenToUnderlying.div(2), { from: user1 }) + }) + }) + + describe('Repay', () => { + beforeEach(async () => { + otoken = await Otoken.new() + + await otoken.init(addressBook.address, weth.address, usdc.address, usdc.address, strikePrice, expiry, isPut, { + from: owner, + }) + + await marginPool.setBorrowerWhitelistedStatus(random, true, { from: owner }) + await marginPool.setBorrowerWhitelistedStatus(user1, true, { from: owner }) + await otoken.mintOtoken(user1, otokenAmount, { from: controllerAddress }) + await marginPool.setBorrowPCT(await otoken.collateralAsset(), TOTAL_PCT, { from: owner }) + await whitelist.whitelistOtoken(otoken.address) + }) + + it('should revert borrowing if repaying 0 of underlying', async () => { + await expectRevert( + marginPool.repay(otoken.address, 0, { from: user1 }), + 'MarginPool: Cannot repay 0 of underlying', + ) + }) + + it('should revert if repaying more than outstanding borrow', async () => { + const oTokenAmount = new BigNumber(await otoken.balanceOf(user1)) + const oTokenToUnderlying = oTokenAmount.times(new BigNumber(10).exponentiatedBy(10)) + + await otoken.approve(marginPool.address, oTokenAmount, { from: user1 }) + await marginPool.borrow(otoken.address, oTokenAmount, { from: user1 }) + await expectRevert( + marginPool.repay(otoken.address, oTokenToUnderlying.plus(1), { from: user1 }), + 'MarginPool: Repaying more than outstanding borrow amount', + ) + + await usdc.approve(marginPool.address, oTokenToUnderlying, { from: user1 }) + await marginPool.repay(otoken.address, oTokenToUnderlying, { from: user1 }) + }) + + it('should repay the outstanding borrow', async () => { + const oTokenAmount = new BigNumber(await otoken.balanceOf(user1)) + + const oTokenToUnderlying = oTokenAmount.times(new BigNumber(10).exponentiatedBy(10)) + + await otoken.approve(marginPool.address, oTokenAmount, { from: user1 }) + await marginPool.borrow(otoken.address, oTokenAmount, { from: user1 }) + + const oTokenBalanceBefore = await otoken.balanceOf(user1) + const amtBorrowableBefore = await marginPool.borrowable(user1, otoken.address) + const marginPoolBalanceBefore = new BigNumber(await usdc.balanceOf(marginPool.address)) + + await usdc.approve(marginPool.address, oTokenToUnderlying, { from: user1 }) + + await marginPool.repay(otoken.address, oTokenToUnderlying, { from: user1 }) + + const oTokenBalanceAfter = await otoken.balanceOf(user1) + const amtBorrowableAfter = await marginPool.borrowable(user1, otoken.address) + const marginPoolBalanceAfter = new BigNumber(await usdc.balanceOf(marginPool.address)) + + assert.equal( + new BigNumber(oTokenToUnderlying).toString(), + new BigNumber(amtBorrowableAfter).minus(amtBorrowableBefore).toString(), + 'Borrowable amount mismatch', + ) + + assert.equal( + new BigNumber(oTokenToUnderlying).toString(), + new BigNumber(marginPoolBalanceAfter).minus(marginPoolBalanceBefore).toString(), + 'WETH value transfered to margin pool mismatch', + ) + }) + }) + + describe('Repay For', () => { + beforeEach(async () => { + otoken = await Otoken.new() + + await otoken.init(addressBook.address, weth.address, usdc.address, usdc.address, strikePrice, expiry, isPut, { + from: owner, + }) + + await marginPool.setBorrowerWhitelistedStatus(random, true, { from: owner }) + await marginPool.setBorrowerWhitelistedStatus(user1, true, { from: owner }) + await otoken.mintOtoken(user1, otokenAmount, { from: controllerAddress }) + await marginPool.setBorrowPCT(await otoken.collateralAsset(), TOTAL_PCT, { from: owner }) + await whitelist.whitelistOtoken(otoken.address) + }) + + it('should revert if repaying for zero address', async () => { + const oTokenAmount = new BigNumber(await otoken.balanceOf(user1)) + const oTokenToUnderlying = oTokenAmount.times(new BigNumber(10).exponentiatedBy(10)) + + await otoken.approve(marginPool.address, oTokenAmount, { from: user1 }) + await marginPool.borrow(otoken.address, oTokenAmount, { from: user1 }) + await expectRevert( + marginPool.repayFor(otoken.address, oTokenToUnderlying.plus(1), ZERO_ADDR, { from: random }), + 'MarginPool: Borrower cannot be zero address', + ) + + await usdc.approve(marginPool.address, oTokenToUnderlying, { from: user1 }) + await marginPool.repay(otoken.address, oTokenToUnderlying, { from: user1 }) + }) + + it('should revert borrowing if repaying 0 of underlying', async () => { + await expectRevert( + marginPool.repay(otoken.address, 0, { from: user1 }), + 'MarginPool: Cannot repay 0 of underlying', + ) + }) + + it('should revert if repaying more than outstanding borrow', async () => { + const oTokenAmount = new BigNumber(await otoken.balanceOf(user1)) + const oTokenToUnderlying = oTokenAmount.times(new BigNumber(10).exponentiatedBy(10)) + + await otoken.approve(marginPool.address, oTokenAmount, { from: user1 }) + await marginPool.borrow(otoken.address, oTokenAmount, { from: user1 }) + await expectRevert( + marginPool.repayFor(otoken.address, oTokenToUnderlying.plus(1), user1, { from: random }), + 'MarginPool: Repaying more than outstanding borrow amount', + ) + + await usdc.approve(marginPool.address, oTokenToUnderlying, { from: user1 }) + await marginPool.repay(otoken.address, oTokenToUnderlying, { from: user1 }) + }) + + it('should repay the outstanding borrow', async () => { + const oTokenAmount = new BigNumber(await otoken.balanceOf(user1)) + const oTokenToUnderlying = oTokenAmount.times(new BigNumber(10).exponentiatedBy(10)) + + await otoken.approve(marginPool.address, oTokenAmount, { from: user1 }) + await marginPool.borrow(otoken.address, oTokenAmount, { from: user1 }) + + const oTokenBalanceBefore = await otoken.balanceOf(user1) + const amtBorrowableBefore = await marginPool.borrowable(user1, otoken.address) + const marginPoolBalanceBefore = new BigNumber(await usdc.balanceOf(marginPool.address)) + + await usdc.transfer(random, oTokenToUnderlying, { from: user1 }) + + await usdc.approve(marginPool.address, oTokenToUnderlying, { from: random }) + + await marginPool.repayFor(otoken.address, oTokenToUnderlying, user1, { from: random }) + + const oTokenBalanceAfter = await otoken.balanceOf(user1) + const amtBorrowableAfter = await marginPool.borrowable(user1, otoken.address) + const marginPoolBalanceAfter = new BigNumber(await usdc.balanceOf(marginPool.address)) + + assert.equal(oTokenBalanceAfter.toString(), oTokenAmount.toString(), 'oToken amount mismatch') + + assert.equal( + new BigNumber(oTokenToUnderlying).toString(), + new BigNumber(amtBorrowableAfter).minus(amtBorrowableBefore).toString(), + 'Borrowable amount mismatch', + ) + + assert.equal( + new BigNumber(oTokenToUnderlying).toString(), + new BigNumber(marginPoolBalanceAfter).minus(marginPoolBalanceBefore).toString(), + 'WETH value transfered to margin pool mismatch', + ) + }) + }) +}) diff --git a/test/unit-tests/controller.test.ts b/test/unit-tests/controller.test.ts index 5a579af9b..7e458c3c7 100644 --- a/test/unit-tests/controller.test.ts +++ b/test/unit-tests/controller.test.ts @@ -6,6 +6,7 @@ import { MockOracleInstance, MockWhitelistModuleInstance, MarginPoolInstance, + BorrowableMarginPoolInstance, ControllerInstance, AddressBookInstance, OwnedUpgradeabilityProxyInstance, @@ -26,6 +27,7 @@ const MarginCalculator = artifacts.require('MarginCalculator.sol') const MockWhitelistModule = artifacts.require('MockWhitelistModule.sol') const AddressBook = artifacts.require('AddressBook.sol') const MarginPool = artifacts.require('MarginPool.sol') +const BorrowableMarginPool = artifacts.require('BorrowableMarginPool.sol') const Controller = artifacts.require('Controller.sol') const MarginVault = artifacts.require('MarginVault.sol') @@ -59,6 +61,8 @@ contract( let calculator: MarginCalculatorInstance // margin pool module let marginPool: MarginPoolInstance + // margin pool module v2 + let borrowableMarginPool: BorrowableMarginPoolInstance // whitelist module mock let whitelist: MockWhitelistModuleInstance // addressbook module mock @@ -83,10 +87,16 @@ contract( calculator = await MarginCalculator.new(oracle.address) // margin pool deployment marginPool = await MarginPool.new(addressBook.address) + // margin pool v2 deployment + borrowableMarginPool = await BorrowableMarginPool.new(addressBook.address) // whitelist module whitelist = await MockWhitelistModule.new() // set margin pool in addressbook await addressBook.setMarginPool(marginPool.address) + // set borrowable margin pool in addressbook + await addressBook.setAddress(web3.utils.soliditySha3('BORROWABLE_POOL'), borrowableMarginPool.address, { + from: owner, + }) // set calculator in addressbook await addressBook.setMarginCalculator(calculator.address) // set oracle in AddressBook @@ -155,10 +165,6 @@ contract( ) }) - it('should revert when set an already operator', async () => { - await expectRevert(controllerProxy.setOperator(accountOperator1, true, { from: accountOwner1 }), 'C9') - }) - it('should be able to remove operator', async () => { await controllerProxy.setOperator(accountOperator1, false, { from: accountOwner1 }) @@ -168,10 +174,6 @@ contract( 'Operator address mismatch', ) }) - - it('should revert when removing an already removed operator', async () => { - await expectRevert(controllerProxy.setOperator(accountOperator1, false, { from: accountOwner1 }), 'C9') - }) }) describe('Vault', () => { @@ -298,6 +300,10 @@ contract( const vaultCounterBefore = new BigNumber(await controllerProxy.getAccountVaultCounter(accountOwner1)) assert.equal(vaultCounterBefore.toString(), '0', 'vault counter before mismatch') + const finalMarginPool = (await borrowableMarginPool.isWhitelistedOptionsVault(accountOwner1)) + ? borrowableMarginPool.address + : marginPool.address + const actionArgs = [ { actionType: ActionType.OpenVault, @@ -317,7 +323,13 @@ contract( }) it('should open vault from account operator', async () => { + await borrowableMarginPool.setOptionsVaultWhitelistedStatus(accountOwner1, false, { from: owner }) await controllerProxy.setOperator(accountOperator1, true, { from: accountOwner1 }) + + const finalMarginPool = (await borrowableMarginPool.isWhitelistedOptionsVault(accountOwner1)) + ? borrowableMarginPool.address + : marginPool.address + assert.equal( await controllerProxy.isOperator(accountOwner1, accountOperator1), true, @@ -342,6 +354,7 @@ contract( const vaultCounterAfter = new BigNumber(await controllerProxy.getAccountVaultCounter(accountOwner1)) assert.equal(vaultCounterAfter.minus(vaultCounterBefore).toString(), '1', 'vault counter after mismatch') + assert.equal(finalMarginPool, marginPool.address, 'vault margin pool mismatch') }) }) @@ -370,6 +383,9 @@ contract( describe('deposit long otoken', () => { it('should revert depositing a non-whitelisted long otoken into vault', async () => { const vaultCounter = new BigNumber(await controllerProxy.getAccountVaultCounter(accountOwner1)) + const finalMarginPool = (await borrowableMarginPool.isWhitelistedOptionsVault(accountOwner1)) + ? borrowableMarginPool.address + : marginPool.address const longToDeposit = createTokenAmount(20) const actionArgs = [ { @@ -384,7 +400,7 @@ contract( }, ] - await longOtoken.approve(marginPool.address, longToDeposit, { from: accountOwner1 }) + await longOtoken.approve(finalMarginPool, longToDeposit, { from: accountOwner1 }) await expectRevert(controllerProxy.operate(actionArgs, { from: accountOwner1 }), 'C17') }) @@ -394,6 +410,10 @@ contract( const vaultCounter = new BigNumber('100') + const finalMarginPool = (await borrowableMarginPool.isWhitelistedOptionsVault(accountOwner1)) + ? borrowableMarginPool.address + : marginPool.address + const longToDeposit = createTokenAmount(20) const actionArgs = [ { @@ -408,13 +428,15 @@ contract( }, ] - await longOtoken.approve(marginPool.address, longToDeposit, { from: accountOwner1 }) + await longOtoken.approve(finalMarginPool, longToDeposit, { from: accountOwner1 }) await expectRevert(controllerProxy.operate(actionArgs, { from: accountOwner1 }), 'C35') }) it('should revert depositing long from an address that is not the msg.sender nor the owner account address', async () => { const vaultCounter = new BigNumber(await controllerProxy.getAccountVaultCounter(accountOwner1)) - + const finalMarginPool = (await borrowableMarginPool.isWhitelistedOptionsVault(accountOwner1)) + ? borrowableMarginPool.address + : marginPool.address const longToDeposit = createTokenAmount(20) const actionArgs = [ { @@ -429,13 +451,17 @@ contract( }, ] - await longOtoken.approve(marginPool.address, longToDeposit, { from: random }) - await longOtoken.approve(marginPool.address, longToDeposit, { from: accountOperator1 }) + await longOtoken.approve(finalMarginPool, longToDeposit, { from: random }) + await longOtoken.approve(finalMarginPool, longToDeposit, { from: accountOperator1 }) await expectRevert(controllerProxy.operate(actionArgs, { from: accountOperator1 }), 'C16') }) it('should deposit long otoken into vault from account owner', async () => { const vaultCounter = new BigNumber(await controllerProxy.getAccountVaultCounter(accountOwner1)) + + const finalMarginPool = (await borrowableMarginPool.isWhitelistedOptionsVault(accountOwner1)) + ? borrowableMarginPool.address + : marginPool.address const longToDeposit = createTokenAmount(20) const actionArgs = [ { @@ -449,13 +475,13 @@ contract( data: ZERO_ADDR, }, ] - const marginPoolBalanceBefore = new BigNumber(await longOtoken.balanceOf(marginPool.address)) + const marginPoolBalanceBefore = new BigNumber(await longOtoken.balanceOf(finalMarginPool)) const senderBalanceBefore = new BigNumber(await longOtoken.balanceOf(accountOwner1)) - await longOtoken.approve(marginPool.address, longToDeposit, { from: accountOwner1 }) + await longOtoken.approve(finalMarginPool, longToDeposit, { from: accountOwner1 }) await controllerProxy.operate(actionArgs, { from: accountOwner1 }) - const marginPoolBalanceAfter = new BigNumber(await longOtoken.balanceOf(marginPool.address)) + const marginPoolBalanceAfter = new BigNumber(await longOtoken.balanceOf(finalMarginPool)) const senderBalanceAfter = new BigNumber(await longOtoken.balanceOf(accountOwner1)) const vaultAfter = await controllerProxy.getVault(accountOwner1, vaultCounter) @@ -490,6 +516,10 @@ contract( ) const vaultCounter = new BigNumber(await controllerProxy.getAccountVaultCounter(accountOwner1)) + + const finalMarginPool = (await borrowableMarginPool.isWhitelistedOptionsVault(accountOwner1)) + ? borrowableMarginPool.address + : marginPool.address const longToDeposit = createTokenAmount(20) const actionArgs = [ { @@ -503,14 +533,14 @@ contract( data: ZERO_ADDR, }, ] - const marginPoolBalanceBefore = new BigNumber(await longOtoken.balanceOf(marginPool.address)) + const marginPoolBalanceBefore = new BigNumber(await longOtoken.balanceOf(finalMarginPool)) const senderBalanceBefore = new BigNumber(await longOtoken.balanceOf(accountOperator1)) const vaultBefore = await controllerProxy.getVault(accountOwner1, vaultCounter) - await longOtoken.approve(marginPool.address, longToDeposit, { from: accountOperator1 }) + await longOtoken.approve(finalMarginPool, longToDeposit, { from: accountOperator1 }) await controllerProxy.operate(actionArgs, { from: accountOperator1 }) - const marginPoolBalanceAfter = new BigNumber(await longOtoken.balanceOf(marginPool.address)) + const marginPoolBalanceAfter = new BigNumber(await longOtoken.balanceOf(finalMarginPool)) const senderBalanceAfter = new BigNumber(await longOtoken.balanceOf(accountOperator1)) const vaultAfter = await controllerProxy.getVault(accountOwner1, vaultCounter) @@ -539,6 +569,10 @@ contract( it('should execute depositing long otoken into vault in multiple actions', async () => { const vaultCounter = new BigNumber(await controllerProxy.getAccountVaultCounter(accountOwner1)) + + const finalMarginPool = (await borrowableMarginPool.isWhitelistedOptionsVault(accountOwner1)) + ? borrowableMarginPool.address + : marginPool.address const longToDeposit = new BigNumber(createTokenAmount(20)) const actionArgs = [ { @@ -562,16 +596,16 @@ contract( data: ZERO_ADDR, }, ] - const marginPoolBalanceBefore = new BigNumber(await longOtoken.balanceOf(marginPool.address)) + const marginPoolBalanceBefore = new BigNumber(await longOtoken.balanceOf(finalMarginPool)) const senderBalanceBefore = new BigNumber(await longOtoken.balanceOf(accountOwner1)) const vaultBefore = await controllerProxy.getVault(accountOwner1, vaultCounter) - await longOtoken.approve(marginPool.address, longToDeposit.multipliedBy(2).toString(), { + await longOtoken.approve(finalMarginPool, longToDeposit.multipliedBy(2).toString(), { from: accountOwner1, }) await controllerProxy.operate(actionArgs, { from: accountOwner1 }) - const marginPoolBalanceAfter = new BigNumber(await longOtoken.balanceOf(marginPool.address)) + const marginPoolBalanceAfter = new BigNumber(await longOtoken.balanceOf(finalMarginPool)) const senderBalanceAfter = new BigNumber(await longOtoken.balanceOf(accountOwner1)) const vaultAfter = await controllerProxy.getVault(accountOwner1, vaultCounter) @@ -600,6 +634,9 @@ contract( it('should revert depositing long otoken with amount equal to zero', async () => { const vaultCounter = new BigNumber(await controllerProxy.getAccountVaultCounter(accountOwner1)) + const finalMarginPool = (await borrowableMarginPool.isWhitelistedOptionsVault(accountOwner1)) + ? borrowableMarginPool.address + : marginPool.address const longToDeposit = createTokenAmount(20) const actionArgs = [ { @@ -614,7 +651,7 @@ contract( }, ] - await longOtoken.approve(marginPool.address, longToDeposit, { from: accountOwner1 }) + await longOtoken.approve(finalMarginPool, longToDeposit, { from: accountOwner1 }) await expectRevert(controllerProxy.operate(actionArgs, { from: accountOwner1 }), 'V4') }) @@ -637,6 +674,10 @@ contract( await whitelist.whitelistOtoken(expiredLongOtoken.address) const vaultCounter = new BigNumber(await controllerProxy.getAccountVaultCounter(accountOwner1)) + + const finalMarginPool = (await borrowableMarginPool.isWhitelistedOptionsVault(accountOwner1)) + ? borrowableMarginPool.address + : marginPool.address const longToDeposit = createTokenAmount(20) const actionArgs = [ { @@ -651,7 +692,7 @@ contract( }, ] - await expiredLongOtoken.approve(marginPool.address, longToDeposit, { from: accountOwner1 }) + await expiredLongOtoken.approve(finalMarginPool, longToDeposit, { from: accountOwner1 }) await expectRevert(controllerProxy.operate(actionArgs, { from: accountOwner1 }), 'C18') }) @@ -674,6 +715,10 @@ contract( // whitelist otoken await whitelist.whitelistOtoken(secondLongOtoken.address) const vaultCounter = new BigNumber(await controllerProxy.getAccountVaultCounter(accountOwner1)) + + const finalMarginPool = (await borrowableMarginPool.isWhitelistedOptionsVault(accountOwner1)) + ? borrowableMarginPool.address + : marginPool.address const actionArgs = [ { actionType: ActionType.DepositLongOption, @@ -687,7 +732,7 @@ contract( }, ] - await secondLongOtoken.approve(marginPool.address, longToDeposit, { from: accountOwner1 }) + await secondLongOtoken.approve(finalMarginPool, longToDeposit, { from: accountOwner1 }) await expectRevert( controllerProxy.operate(actionArgs, { from: accountOwner1 }), 'MarginCalculator: Too many long otokens in the vault', @@ -696,6 +741,7 @@ contract( it('should revert deposting long from controller implementation contract instead of the controller proxy', async () => { await controllerImplementation.initialize(addressBook.address, owner) + const longToDeposit = createTokenAmount(20) const actionArgs = [ { @@ -720,7 +766,11 @@ contract( }, ] - await longOtoken.approve(marginPool.address, longToDeposit, { from: accountOwner1 }) + const finalMarginPool = (await borrowableMarginPool.isWhitelistedOptionsVault(accountOwner1)) + ? borrowableMarginPool.address + : marginPool.address + await longOtoken.approve(finalMarginPool, longToDeposit, { from: accountOwner1 }) + await expectRevert( controllerImplementation.operate(actionArgs, { from: accountOwner1 }), 'MarginPool: Sender is not Controller', @@ -731,6 +781,7 @@ contract( describe('withdraw long otoken', () => { it('should revert withdrawing long otoken with wrong index from a vault', async () => { const vaultCounter = new BigNumber(await controllerProxy.getAccountVaultCounter(accountOwner1)) + assert.isAbove(vaultCounter.toNumber(), 0, 'Account owner have no vault') const longToWithdraw = createTokenAmount(20) @@ -818,6 +869,9 @@ contract( it('should withdraw long otoken to any random address where msg.sender is account owner', async () => { const vaultCounter = new BigNumber(await controllerProxy.getAccountVaultCounter(accountOwner1)) + const finalMarginPool = (await borrowableMarginPool.isWhitelistedOptionsVault(accountOwner1)) + ? borrowableMarginPool.address + : marginPool.address assert.isAbove(vaultCounter.toNumber(), 0, 'Account owner have no vault') const longToWithdraw = createTokenAmount(10) @@ -833,13 +887,13 @@ contract( data: ZERO_ADDR, }, ] - const marginPoolBalanceBefore = new BigNumber(await longOtoken.balanceOf(marginPool.address)) + const marginPoolBalanceBefore = new BigNumber(await longOtoken.balanceOf(finalMarginPool)) const receiverBalanceBefore = new BigNumber(await longOtoken.balanceOf(random)) const vaultBefore = await controllerProxy.getVault(accountOwner1, vaultCounter) await controllerProxy.operate(actionArgs, { from: accountOwner1 }) - const marginPoolBalanceAfter = new BigNumber(await longOtoken.balanceOf(marginPool.address)) + const marginPoolBalanceAfter = new BigNumber(await longOtoken.balanceOf(finalMarginPool)) const receiverBalanceAfter = new BigNumber(await longOtoken.balanceOf(random)) const vaultAfter = await controllerProxy.getVault(accountOwner1, vaultCounter) @@ -869,6 +923,9 @@ contract( ) const vaultCounter = new BigNumber(await controllerProxy.getAccountVaultCounter(accountOwner1)) + const finalMarginPool = (await borrowableMarginPool.isWhitelistedOptionsVault(accountOwner1)) + ? borrowableMarginPool.address + : marginPool.address assert.isAbove(vaultCounter.toNumber(), 0, 'Account owner have no vault') const longToWithdraw = createTokenAmount(10) @@ -884,13 +941,13 @@ contract( data: ZERO_ADDR, }, ] - const marginPoolBalanceBefore = new BigNumber(await longOtoken.balanceOf(marginPool.address)) + const marginPoolBalanceBefore = new BigNumber(await longOtoken.balanceOf(finalMarginPool)) const receiverBalanceBefore = new BigNumber(await longOtoken.balanceOf(random)) const vaultBefore = await controllerProxy.getVault(accountOwner1, vaultCounter) await controllerProxy.operate(actionArgs, { from: accountOperator1 }) - const marginPoolBalanceAfter = new BigNumber(await longOtoken.balanceOf(marginPool.address)) + const marginPoolBalanceAfter = new BigNumber(await longOtoken.balanceOf(finalMarginPool)) const receiverBalanceAfter = new BigNumber(await longOtoken.balanceOf(random)) const vaultAfter = await controllerProxy.getVault(accountOwner1, vaultCounter) @@ -914,6 +971,9 @@ contract( it('should execute withdrawing long otoken in mutliple actions', async () => { const vaultCounter = new BigNumber(await controllerProxy.getAccountVaultCounter(accountOwner1)) + const finalMarginPool = (await borrowableMarginPool.isWhitelistedOptionsVault(accountOwner1)) + ? borrowableMarginPool.address + : marginPool.address assert.isAbove(vaultCounter.toNumber(), 0, 'Account owner have no vault') const longToWithdraw = new BigNumber(createTokenAmount(10)) @@ -939,13 +999,13 @@ contract( data: ZERO_ADDR, }, ] - const marginPoolBalanceBefore = new BigNumber(await longOtoken.balanceOf(marginPool.address)) + const marginPoolBalanceBefore = new BigNumber(await longOtoken.balanceOf(finalMarginPool)) const receiverBalanceBefore = new BigNumber(await longOtoken.balanceOf(accountOwner1)) const vaultBefore = await controllerProxy.getVault(accountOwner1, vaultCounter) await controllerProxy.operate(actionArgs, { from: accountOwner1 }) - const marginPoolBalanceAfter = new BigNumber(await longOtoken.balanceOf(marginPool.address)) + const marginPoolBalanceAfter = new BigNumber(await longOtoken.balanceOf(finalMarginPool)) const receiverBalanceAfter = new BigNumber(await longOtoken.balanceOf(accountOwner1)) const vaultAfter = await controllerProxy.getVault(accountOwner1, vaultCounter) @@ -969,6 +1029,9 @@ contract( it('should remove otoken address from otoken array if amount is equal to zero after withdrawing', async () => { const vaultCounter = new BigNumber(await controllerProxy.getAccountVaultCounter(accountOwner1)) + const finalMarginPool = (await borrowableMarginPool.isWhitelistedOptionsVault(accountOwner1)) + ? borrowableMarginPool.address + : marginPool.address assert.isAbove(vaultCounter.toNumber(), 0, 'Account owner have no vault') const vaultBefore = await controllerProxy.getVault(accountOwner1, vaultCounter) @@ -986,12 +1049,12 @@ contract( data: ZERO_ADDR, }, ] - const marginPoolBalanceBefore = new BigNumber(await longOtoken.balanceOf(marginPool.address)) + const marginPoolBalanceBefore = new BigNumber(await longOtoken.balanceOf(finalMarginPool)) const receiverBalanceBefore = new BigNumber(await longOtoken.balanceOf(accountOwner1)) await controllerProxy.operate(actionArgs, { from: accountOwner1 }) - const marginPoolBalanceAfter = new BigNumber(await longOtoken.balanceOf(marginPool.address)) + const marginPoolBalanceAfter = new BigNumber(await longOtoken.balanceOf(finalMarginPool)) const receiverBalanceAfter = new BigNumber(await longOtoken.balanceOf(accountOwner1)) const vaultAfter = await controllerProxy.getVault(accountOwner1, vaultCounter) @@ -1037,6 +1100,12 @@ contract( await whitelist.whitelistOtoken(expiredLongOtoken.address, { from: owner }) // deposit long otoken into vault const vaultId = new BigNumber('1') + + const finalMarginPool = + (await controllerProxy.getVaultWithDetails(accountOwner1, vaultId))[1].toNumber() < 2 + ? marginPool.address + : borrowableMarginPool.address + const actionArgs = [ { actionType: ActionType.DepositLongOption, @@ -1049,7 +1118,7 @@ contract( data: ZERO_ADDR, }, ] - await expiredLongOtoken.approve(marginPool.address, longToDeposit, { from: accountOwner1 }) + await expiredLongOtoken.approve(finalMarginPool, longToDeposit, { from: accountOwner1 }) await controllerProxy.operate(actionArgs, { from: accountOwner1 }) const vaultAfter = await controllerProxy.getVault(accountOwner1, vaultId) assert.equal(vaultAfter.longOtokens.length, 1, 'Vault long otoken array length mismatch') @@ -1103,6 +1172,9 @@ contract( // whitelist usdc await whitelist.whitelistCollateral(usdc.address) const vaultCounter = new BigNumber(await controllerProxy.getAccountVaultCounter(accountOwner1)) + const finalMarginPool = (await borrowableMarginPool.isWhitelistedOptionsVault(accountOwner1)) + ? borrowableMarginPool.address + : marginPool.address assert.isAbove(vaultCounter.toNumber(), 0, 'Account owner have no vault') const collateralToDeposit = createTokenAmount(10, usdcDecimals) @@ -1119,13 +1191,13 @@ contract( }, ] - const marginPoolBalanceBefore = new BigNumber(await usdc.balanceOf(marginPool.address)) + const marginPoolBalanceBefore = new BigNumber(await usdc.balanceOf(finalMarginPool)) const senderBalanceBefore = new BigNumber(await usdc.balanceOf(accountOwner1)) - await usdc.approve(marginPool.address, collateralToDeposit, { from: accountOwner1 }) + await usdc.approve(finalMarginPool, collateralToDeposit, { from: accountOwner1 }) await controllerProxy.operate(actionArgs, { from: accountOwner1 }) - const marginPoolBalanceAfter = new BigNumber(await usdc.balanceOf(marginPool.address)) + const marginPoolBalanceAfter = new BigNumber(await usdc.balanceOf(finalMarginPool)) const senderBalanceAfter = new BigNumber(await usdc.balanceOf(accountOwner1)) const vaultAfter = await controllerProxy.getVault(accountOwner1, vaultCounter) @@ -1154,6 +1226,9 @@ contract( it('should deposit a whitelisted collateral asset from account operator', async () => { const vaultCounter = new BigNumber(await controllerProxy.getAccountVaultCounter(accountOwner1)) + const finalMarginPool = (await borrowableMarginPool.isWhitelistedOptionsVault(accountOwner1)) + ? borrowableMarginPool.address + : marginPool.address assert.isAbove(vaultCounter.toNumber(), 0, 'Account owner have no vault') const collateralToDeposit = createTokenAmount(10, usdcDecimals) @@ -1170,14 +1245,14 @@ contract( }, ] - const marginPoolBalanceBefore = new BigNumber(await usdc.balanceOf(marginPool.address)) + const marginPoolBalanceBefore = new BigNumber(await usdc.balanceOf(finalMarginPool)) const senderBalanceBefore = new BigNumber(await usdc.balanceOf(accountOperator1)) const vaultBefore = await controllerProxy.getVault(accountOwner1, vaultCounter) - await usdc.approve(marginPool.address, collateralToDeposit, { from: accountOperator1 }) + await usdc.approve(finalMarginPool, collateralToDeposit, { from: accountOperator1 }) await controllerProxy.operate(actionArgs, { from: accountOperator1 }) - const marginPoolBalanceAfter = new BigNumber(await usdc.balanceOf(marginPool.address)) + const marginPoolBalanceAfter = new BigNumber(await usdc.balanceOf(finalMarginPool)) const senderBalanceAfter = new BigNumber(await usdc.balanceOf(accountOperator1)) const vaultAfter = await controllerProxy.getVault(accountOwner1, vaultCounter) @@ -1223,13 +1298,19 @@ contract( }, ] - await usdc.approve(marginPool.address, collateralToDeposit, { from: accountOwner1 }) + const finalMarginPool = (await borrowableMarginPool.isWhitelistedOptionsVault(accountOwner1)) + ? borrowableMarginPool.address + : marginPool.address + + await usdc.approve(finalMarginPool, collateralToDeposit, { from: accountOwner1 }) await expectRevert(controllerProxy.operate(actionArgs, { from: accountOwner1 }), 'C35') }) it('should revert depositing long from an address that is not the msg.sender nor the owner account address', async () => { const vaultCounter = new BigNumber(await controllerProxy.getAccountVaultCounter(accountOwner1)) - + const finalMarginPool = (await borrowableMarginPool.isWhitelistedOptionsVault(accountOwner1)) + ? borrowableMarginPool.address + : marginPool.address const collateralToDeposit = createTokenAmount(10, usdcDecimals) const actionArgs = [ { @@ -1244,13 +1325,16 @@ contract( }, ] - await usdc.approve(marginPool.address, collateralToDeposit, { from: random }) - await usdc.approve(marginPool.address, collateralToDeposit, { from: accountOperator1 }) + await usdc.approve(finalMarginPool, collateralToDeposit, { from: random }) + await usdc.approve(finalMarginPool, collateralToDeposit, { from: accountOperator1 }) await expectRevert(controllerProxy.operate(actionArgs, { from: accountOperator1 }), 'C20') }) it('should revert depositing a collateral asset with amount equal to zero', async () => { const vaultCounter = new BigNumber(await controllerProxy.getAccountVaultCounter(accountOwner1)) + const finalMarginPool = (await borrowableMarginPool.isWhitelistedOptionsVault(accountOwner1)) + ? borrowableMarginPool.address + : marginPool.address assert.isAbove(vaultCounter.toNumber(), 0, 'Account owner have no vault') const collateralToDeposit = createTokenAmount(0, usdcDecimals) @@ -1267,12 +1351,15 @@ contract( }, ] - await usdc.approve(marginPool.address, collateralToDeposit, { from: accountOwner1 }) + await usdc.approve(finalMarginPool, collateralToDeposit, { from: accountOwner1 }) await expectRevert(controllerProxy.operate(actionArgs, { from: accountOwner1 }), 'V7') }) it('should execute depositing collateral into vault in multiple actions', async () => { const vaultCounter = new BigNumber(await controllerProxy.getAccountVaultCounter(accountOwner1)) + const finalMarginPool = (await borrowableMarginPool.isWhitelistedOptionsVault(accountOwner1)) + ? borrowableMarginPool.address + : marginPool.address const collateralToDeposit = new BigNumber(createTokenAmount(20, usdcDecimals)) const actionArgs = [ { @@ -1296,14 +1383,14 @@ contract( data: ZERO_ADDR, }, ] - const marginPoolBalanceBefore = new BigNumber(await usdc.balanceOf(marginPool.address)) + const marginPoolBalanceBefore = new BigNumber(await usdc.balanceOf(finalMarginPool)) const senderBalanceBefore = new BigNumber(await usdc.balanceOf(accountOwner1)) const vaultBefore = await controllerProxy.getVault(accountOwner1, vaultCounter) - await usdc.approve(marginPool.address, collateralToDeposit.multipliedBy(2), { from: accountOwner1 }) + await usdc.approve(finalMarginPool, collateralToDeposit.multipliedBy(2), { from: accountOwner1 }) await controllerProxy.operate(actionArgs, { from: accountOwner1 }) - const marginPoolBalanceAfter = new BigNumber(await usdc.balanceOf(marginPool.address)) + const marginPoolBalanceAfter = new BigNumber(await usdc.balanceOf(finalMarginPool)) const senderBalanceAfter = new BigNumber(await usdc.balanceOf(accountOwner1)) const vaultAfter = await controllerProxy.getVault(accountOwner1, vaultCounter) @@ -1339,6 +1426,9 @@ contract( await trx.mint(accountOwner1, new BigNumber('1000')) const vaultCounter = new BigNumber(await controllerProxy.getAccountVaultCounter(accountOwner1)) + const finalMarginPool = (await borrowableMarginPool.isWhitelistedOptionsVault(accountOwner1)) + ? borrowableMarginPool.address + : marginPool.address assert.isAbove(vaultCounter.toNumber(), 0, 'Account owner have no vault') const collateralDeposit = createTokenAmount(10, wethDecimals) @@ -1355,7 +1445,7 @@ contract( }, ] - await trx.approve(marginPool.address, collateralDeposit, { from: accountOwner1 }) + await trx.approve(finalMarginPool, collateralDeposit, { from: accountOwner1 }) await expectRevert(controllerProxy.operate(actionArgs, { from: accountOwner1 }), 'C21') }) }) @@ -1367,6 +1457,9 @@ contract( await weth.mint(accountOwner1, collateralToDeposit) const vaultCounter = new BigNumber(await controllerProxy.getAccountVaultCounter(accountOwner1)) + const finalMarginPool = (await borrowableMarginPool.isWhitelistedOptionsVault(accountOwner1)) + ? borrowableMarginPool.address + : marginPool.address const actionArgs = [ { actionType: ActionType.DepositCollateral, @@ -1380,7 +1473,7 @@ contract( }, ] - await weth.approve(marginPool.address, collateralToDeposit, { from: accountOwner1 }) + await weth.approve(finalMarginPool, collateralToDeposit, { from: accountOwner1 }) await expectRevert( controllerProxy.operate(actionArgs, { from: accountOwner1 }), 'MarginCalculator: Too many collateral assets in the vault', @@ -1458,6 +1551,9 @@ contract( it('should withdraw collateral to any random address where msg.sender is account owner', async () => { const vaultCounter = new BigNumber(await controllerProxy.getAccountVaultCounter(accountOwner1)) + const finalMarginPool = (await borrowableMarginPool.isWhitelistedOptionsVault(accountOwner1)) + ? borrowableMarginPool.address + : marginPool.address assert.isAbove(vaultCounter.toNumber(), 0, 'Account owner have no vault') const collateralToWithdraw = createTokenAmount(10, usdcDecimals) @@ -1473,13 +1569,13 @@ contract( data: ZERO_ADDR, }, ] - const marginPoolBalanceBefore = new BigNumber(await usdc.balanceOf(marginPool.address)) + const marginPoolBalanceBefore = new BigNumber(await usdc.balanceOf(finalMarginPool)) const receiverBalanceBefore = new BigNumber(await usdc.balanceOf(random)) const vaultBefore = await controllerProxy.getVault(accountOwner1, vaultCounter) await controllerProxy.operate(actionArgs, { from: accountOwner1 }) - const marginPoolBalanceAfter = new BigNumber(await usdc.balanceOf(marginPool.address)) + const marginPoolBalanceAfter = new BigNumber(await usdc.balanceOf(finalMarginPool)) const receiverBalanceAfter = new BigNumber(await usdc.balanceOf(random)) const vaultAfter = await controllerProxy.getVault(accountOwner1, vaultCounter) @@ -1511,6 +1607,10 @@ contract( ) const vaultCounter = new BigNumber(await controllerProxy.getAccountVaultCounter(accountOwner1)) + + const finalMarginPool = (await borrowableMarginPool.isWhitelistedOptionsVault(accountOwner1)) + ? borrowableMarginPool.address + : marginPool.address assert.isAbove(vaultCounter.toNumber(), 0, 'Account owner have no vault') const collateralToWithdraw = createTokenAmount(10, usdcDecimals) @@ -1526,13 +1626,13 @@ contract( data: ZERO_ADDR, }, ] - const marginPoolBalanceBefore = new BigNumber(await usdc.balanceOf(marginPool.address)) + const marginPoolBalanceBefore = new BigNumber(await usdc.balanceOf(finalMarginPool)) const receiverBalanceBefore = new BigNumber(await usdc.balanceOf(random)) const vaultBefore = await controllerProxy.getVault(accountOwner1, vaultCounter) await controllerProxy.operate(actionArgs, { from: accountOperator1 }) - const marginPoolBalanceAfter = new BigNumber(await usdc.balanceOf(marginPool.address)) + const marginPoolBalanceAfter = new BigNumber(await usdc.balanceOf(finalMarginPool)) const receiverBalanceAfter = new BigNumber(await usdc.balanceOf(random)) const vaultAfter = await controllerProxy.getVault(accountOwner1, vaultCounter) @@ -1558,6 +1658,9 @@ contract( it('should execute withdrawing collateral asset in mutliple actions', async () => { const vaultCounter = new BigNumber(await controllerProxy.getAccountVaultCounter(accountOwner1)) + const finalMarginPool = (await borrowableMarginPool.isWhitelistedOptionsVault(accountOwner1)) + ? borrowableMarginPool.address + : marginPool.address assert.isAbove(vaultCounter.toNumber(), 0, 'Account owner have no vault') const collateralToWithdraw = new BigNumber(createTokenAmount(10, usdcDecimals)) @@ -1583,13 +1686,13 @@ contract( data: ZERO_ADDR, }, ] - const marginPoolBalanceBefore = new BigNumber(await usdc.balanceOf(marginPool.address)) + const marginPoolBalanceBefore = new BigNumber(await usdc.balanceOf(finalMarginPool)) const receiverBalanceBefore = new BigNumber(await usdc.balanceOf(accountOwner1)) const vaultBefore = await controllerProxy.getVault(accountOwner1, vaultCounter) await controllerProxy.operate(actionArgs, { from: accountOwner1 }) - const marginPoolBalanceAfter = new BigNumber(await usdc.balanceOf(marginPool.address)) + const marginPoolBalanceAfter = new BigNumber(await usdc.balanceOf(finalMarginPool)) const receiverBalanceAfter = new BigNumber(await usdc.balanceOf(accountOwner1)) const vaultAfter = await controllerProxy.getVault(accountOwner1, vaultCounter) @@ -1615,6 +1718,9 @@ contract( it('should remove collateral asset address from collateral array if amount is equal to zero after withdrawing', async () => { const vaultCounter = new BigNumber(await controllerProxy.getAccountVaultCounter(accountOwner1)) + const finalMarginPool = (await borrowableMarginPool.isWhitelistedOptionsVault(accountOwner1)) + ? borrowableMarginPool.address + : marginPool.address assert.isAbove(vaultCounter.toNumber(), 0, 'Account owner have no vault') const vaultBefore = await controllerProxy.getVault(accountOwner1, vaultCounter) @@ -1632,12 +1738,12 @@ contract( data: ZERO_ADDR, }, ] - const marginPoolBalanceBefore = new BigNumber(await usdc.balanceOf(marginPool.address)) + const marginPoolBalanceBefore = new BigNumber(await usdc.balanceOf(finalMarginPool)) const receiverBalanceBefore = new BigNumber(await usdc.balanceOf(accountOwner1)) await controllerProxy.operate(actionArgs, { from: accountOwner1 }) - const marginPoolBalanceAfter = new BigNumber(await usdc.balanceOf(marginPool.address)) + const marginPoolBalanceAfter = new BigNumber(await usdc.balanceOf(finalMarginPool)) const receiverBalanceAfter = new BigNumber(await usdc.balanceOf(accountOwner1)) const vaultAfter = await controllerProxy.getVault(accountOwner1, vaultCounter) @@ -1732,6 +1838,9 @@ contract( it('should revert minting using un-marginable collateral asset', async () => { const vaultCounter = new BigNumber(await controllerProxy.getAccountVaultCounter(accountOwner1)) + const finalMarginPool = (await borrowableMarginPool.isWhitelistedOptionsVault(accountOwner1)) + ? borrowableMarginPool.address + : marginPool.address assert.isAbove(vaultCounter.toNumber(), 0, 'Account owner have no vault') const collateralToDeposit = new BigNumber(await shortOtoken.strikePrice()).dividedBy(1e8) @@ -1762,7 +1871,7 @@ contract( // free money await weth.mint(accountOwner1, collateralToDeposit) - await weth.approve(marginPool.address, collateralToDeposit, { from: accountOwner1 }) + await weth.approve(finalMarginPool, collateralToDeposit, { from: accountOwner1 }) await expectRevert( controllerProxy.operate(actionArgs, { from: accountOwner1 }), 'MarginCalculator: collateral asset not marginable for short asset', @@ -1791,6 +1900,9 @@ contract( it('mint naked short otoken from owner', async () => { const vaultCounter = new BigNumber(await controllerProxy.getAccountVaultCounter(accountOwner1)) + const finalMarginPool = (await borrowableMarginPool.isWhitelistedOptionsVault(accountOwner1)) + ? borrowableMarginPool.address + : marginPool.address assert.isAbove(vaultCounter.toNumber(), 0, 'Account owner have no vault') const collateralToDeposit = new BigNumber(await shortOtoken.strikePrice()).dividedBy(100) @@ -1818,15 +1930,15 @@ contract( }, ] - const marginPoolBalanceBefore = new BigNumber(await usdc.balanceOf(marginPool.address)) + const marginPoolBalanceBefore = new BigNumber(await usdc.balanceOf(finalMarginPool)) const senderBalanceBefore = new BigNumber(await usdc.balanceOf(accountOwner1)) const senderShortBalanceBefore = new BigNumber(await shortOtoken.balanceOf(accountOwner1)) const vaultBefore = await controllerProxy.getVault(accountOwner1, vaultCounter) - await usdc.approve(marginPool.address, collateralToDeposit, { from: accountOwner1 }) + await usdc.approve(finalMarginPool, collateralToDeposit, { from: accountOwner1 }) await controllerProxy.operate(actionArgs, { from: accountOwner1 }) - const marginPoolBalanceAfter = new BigNumber(await usdc.balanceOf(marginPool.address)) + const marginPoolBalanceAfter = new BigNumber(await usdc.balanceOf(finalMarginPool)) const senderBalanceAfter = new BigNumber(await usdc.balanceOf(accountOwner1)) const senderShortBalanceAfter = new BigNumber(await shortOtoken.balanceOf(accountOwner1)) const vaultAfter = await controllerProxy.getVault(accountOwner1, vaultCounter) @@ -1874,6 +1986,9 @@ contract( it('mint naked short otoken from operator', async () => { const vaultCounter = new BigNumber(await controllerProxy.getAccountVaultCounter(accountOwner1)) + const finalMarginPool = (await borrowableMarginPool.isWhitelistedOptionsVault(accountOwner1)) + ? borrowableMarginPool.address + : marginPool.address assert.isAbove(vaultCounter.toNumber(), 0, 'Account owner have no vault') const collateralToDeposit = new BigNumber(await shortOtoken.strikePrice()).dividedBy(100) @@ -1901,15 +2016,15 @@ contract( }, ] - const marginPoolBalanceBefore = new BigNumber(await usdc.balanceOf(marginPool.address)) + const marginPoolBalanceBefore = new BigNumber(await usdc.balanceOf(finalMarginPool)) const senderBalanceBefore = new BigNumber(await usdc.balanceOf(accountOperator1)) const senderShortBalanceBefore = new BigNumber(await shortOtoken.balanceOf(accountOperator1)) const vaultBefore = await controllerProxy.getVault(accountOwner1, vaultCounter) - await usdc.approve(marginPool.address, collateralToDeposit, { from: accountOperator1 }) + await usdc.approve(finalMarginPool, collateralToDeposit, { from: accountOperator1 }) await controllerProxy.operate(actionArgs, { from: accountOperator1 }) - const marginPoolBalanceAfter = new BigNumber(await usdc.balanceOf(marginPool.address)) + const marginPoolBalanceAfter = new BigNumber(await usdc.balanceOf(finalMarginPool)) const senderBalanceAfter = new BigNumber(await usdc.balanceOf(accountOperator1)) const senderShortBalanceAfter = new BigNumber(await shortOtoken.balanceOf(accountOperator1)) const vaultAfter = await controllerProxy.getVault(accountOwner1, vaultCounter) @@ -1988,6 +2103,9 @@ contract( it('should withdraw exceeded collateral from naked short position when net value > 0 ', async () => { const vaultCounter = new BigNumber(await controllerProxy.getAccountVaultCounter(accountOwner1)) + const finalMarginPool = (await borrowableMarginPool.isWhitelistedOptionsVault(accountOwner1)) + ? borrowableMarginPool.address + : marginPool.address assert.isAbove(vaultCounter.toNumber(), 0, 'Account owner have no vault') // deposit more collateral @@ -2004,11 +2122,11 @@ contract( data: ZERO_ADDR, }, ] - await usdc.approve(marginPool.address, excessCollateralToDeposit, { from: accountOwner1 }) + await usdc.approve(finalMarginPool, excessCollateralToDeposit, { from: accountOwner1 }) await controllerProxy.operate(firstActionArgs, { from: accountOwner1 }) const vaultBefore = await controllerProxy.getVaultWithDetails(accountOwner1, vaultCounter) - const marginPoolBalanceBefore = new BigNumber(await usdc.balanceOf(marginPool.address)) + const marginPoolBalanceBefore = new BigNumber(await usdc.balanceOf(finalMarginPool)) const withdrawerBalanceBefore = new BigNumber(await usdc.balanceOf(accountOwner1)) const [netValue, isExcess] = await calculator.getExcessCollateral(vaultBefore[0], vaultBefore[1]) @@ -2035,7 +2153,7 @@ contract( await controllerProxy.operate(secondActionArgs, { from: accountOwner1 }) const vaultAfter = await controllerProxy.getVault(accountOwner1, vaultCounter) - const marginPoolBalanceAfter = new BigNumber(await usdc.balanceOf(marginPool.address)) + const marginPoolBalanceAfter = new BigNumber(await usdc.balanceOf(finalMarginPool)) const withdrawerBalanceAfter = new BigNumber(await usdc.balanceOf(accountOwner1)) assert.equal( @@ -2135,7 +2253,11 @@ contract( }, ] - await usdc.approve(marginPool.address, collateralToDeposit, { from: accountOperator1 }) + const finalMarginPool = (await borrowableMarginPool.isWhitelistedOptionsVault(accountOwner1)) + ? borrowableMarginPool.address + : marginPool.address + + await usdc.approve(finalMarginPool, collateralToDeposit, { from: accountOperator1 }) await expectRevert(controllerProxy.operate(actionArgs, { from: accountOperator1 }), 'C23') }) }) @@ -2481,6 +2603,7 @@ contract( before(async () => { const vaultCounterBefore = new BigNumber(await controllerProxy.getAccountVaultCounter(accountOwner1)) + const expiryTime = new BigNumber(60 * 60) // after 1 hour expiredShortOtoken = await MockOtoken.new() // init otoken @@ -2532,13 +2655,17 @@ contract( }, ] - const marginPoolBalanceBefore = new BigNumber(await usdc.balanceOf(marginPool.address)) + const finalMarginPool = (await borrowableMarginPool.isWhitelistedOptionsVault(accountOwner1)) + ? borrowableMarginPool.address + : marginPool.address + + const marginPoolBalanceBefore = new BigNumber(await usdc.balanceOf(finalMarginPool)) const senderBalanceBefore = new BigNumber(await usdc.balanceOf(accountOwner1)) - await usdc.approve(marginPool.address, collateralToDeposit, { from: accountOwner1 }) + await usdc.approve(finalMarginPool, collateralToDeposit, { from: accountOwner1 }) await controllerProxy.operate(actionArgs, { from: accountOwner1 }) - const marginPoolBalanceAfter = new BigNumber(await usdc.balanceOf(marginPool.address)) + const marginPoolBalanceAfter = new BigNumber(await usdc.balanceOf(finalMarginPool)) const senderBalanceAfter = new BigNumber(await usdc.balanceOf(accountOwner1)) assert.equal( @@ -2583,6 +2710,9 @@ contract( it('should revert minting an expired short otoken', async () => { const vaultCounter = new BigNumber(await controllerProxy.getAccountVaultCounter(accountOwner1)) + const finalMarginPool = (await borrowableMarginPool.isWhitelistedOptionsVault(accountOwner1)) + ? borrowableMarginPool.address + : marginPool.address assert.isAbove(vaultCounter.toNumber(), 0, 'Account owner have no vault') const collateralToDeposit = new BigNumber(await expiredShortOtoken.strikePrice()).dividedBy(100) @@ -2610,7 +2740,7 @@ contract( }, ] - await usdc.approve(marginPool.address, collateralToDeposit, { from: accountOperator1 }) + await usdc.approve(finalMarginPool, collateralToDeposit, { from: accountOperator1 }) await expectRevert(controllerProxy.operate(actionArgs, { from: accountOwner1 }), 'C24') }) @@ -2641,6 +2771,7 @@ contract( describe('Redeem', () => { let shortOtoken: MockOtokenInstance let fakeOtoken: MockOtokenInstance + let finalMarginPool: string before(async () => { const expiryTime = new BigNumber(60 * 60 * 24) // after 1 day @@ -2707,11 +2838,19 @@ contract( data: ZERO_ADDR, }, ] - await usdc.approve(marginPool.address, collateralToDeposit, { from: accountOwner1 }) + + finalMarginPool = + !(await borrowableMarginPool.isWhitelistedOTokenBuyer(holder1)) || + new BigNumber(await usdc.balanceOf(borrowableMarginPool.address)).isLessThan(collateralToDeposit) + ? marginPool.address + : borrowableMarginPool.address + + await usdc.approve(finalMarginPool, collateralToDeposit, { from: accountOwner1 }) await controllerProxy.operate(actionArgs, { from: accountOwner1 }) // transfer minted short otoken to hodler` await shortOtoken.transfer(holder1, amountToMint.toString(), { from: accountOwner1 }) }) + it('should revert exercising non-whitelisted otoken', async () => { const shortAmountToBurn = new BigNumber('1') const actionArgs = [ @@ -2837,13 +2976,13 @@ contract( assert.equal(await controllerProxy.hasExpired(shortOtoken.address), true, 'Short otoken is not expired yet') const payout = createTokenAmount(50, usdcDecimals) - const marginPoolBalanceBefore = new BigNumber(await usdc.balanceOf(marginPool.address)) + const marginPoolBalanceBefore = new BigNumber(await usdc.balanceOf(finalMarginPool)) const senderBalanceBefore = new BigNumber(await usdc.balanceOf(holder1)) const senderShortBalanceBefore = new BigNumber(await shortOtoken.balanceOf(holder1)) await controllerProxy.operate(actionArgs, { from: holder1 }) - const marginPoolBalanceAfter = new BigNumber(await usdc.balanceOf(marginPool.address)) + const marginPoolBalanceAfter = new BigNumber(await usdc.balanceOf(finalMarginPool)) const senderBalanceAfter = new BigNumber(await usdc.balanceOf(holder1)) const senderShortBalanceAfter = new BigNumber(await shortOtoken.balanceOf(holder1)) @@ -2881,7 +3020,7 @@ contract( const vaultCounter = new BigNumber(await controllerProxy.getAccountVaultCounter(accountOwner1)).plus(1) const amountCollateral = createTokenAmount(1, wethDecimals) await weth.mint(accountOwner1, amountCollateral) - await weth.approve(marginPool.address, amountCollateral, { from: accountOwner1 }) + await weth.approve(finalMarginPool, amountCollateral, { from: accountOwner1 }) const amountOtoken = createTokenAmount(1) const actionArgs = [ { @@ -2965,7 +3104,7 @@ contract( const vaultCounter = new BigNumber(await controllerProxy.getAccountVaultCounter(accountOwner1)).plus(1) const amountCollateral = createTokenAmount(1, wethDecimals) await weth2.mint(accountOwner1, amountCollateral) - await weth2.approve(marginPool.address, amountCollateral, { from: accountOwner1 }) + await weth2.approve(finalMarginPool, amountCollateral, { from: accountOwner1 }) const amountOtoken = createTokenAmount(1) const actionArgs = [ @@ -3095,7 +3234,7 @@ contract( data: ZERO_ADDR, }, ] - await usdc.approve(marginPool.address, firstCollateralToDeposit, { from: accountOwner1 }) + await usdc.approve(finalMarginPool, firstCollateralToDeposit, { from: accountOwner1 }) await controllerProxy.operate(actionArgs, { from: accountOwner1 }) vaultCounter = new BigNumber(await controllerProxy.getAccountVaultCounter(accountOwner1)).plus(1) @@ -3131,7 +3270,7 @@ contract( data: ZERO_ADDR, }, ] - await usdc.approve(marginPool.address, firstCollateralToDeposit, { from: accountOwner1 }) + await usdc.approve(finalMarginPool, firstCollateralToDeposit, { from: accountOwner1 }) await controllerProxy.operate(actionArgs, { from: accountOwner1 }) // transfer minted short otoken to hodler await firstOtoken.transfer(holder1, amountToMint, { from: accountOwner1 }) @@ -3193,12 +3332,12 @@ contract( ] const payout = createTokenAmount(100, usdcDecimals) - const marginPoolBalanceBefore = new BigNumber(await usdc.balanceOf(marginPool.address)) + const marginPoolBalanceBefore = new BigNumber(await usdc.balanceOf(finalMarginPool)) const senderBalanceBefore = new BigNumber(await usdc.balanceOf(holder1)) await controllerProxy.operate(actionArgs, { from: holder1 }) - const marginPoolBalanceAfter = new BigNumber(await usdc.balanceOf(marginPool.address)) + const marginPoolBalanceAfter = new BigNumber(await usdc.balanceOf(finalMarginPool)) const senderBalanceAfter = new BigNumber(await usdc.balanceOf(holder1)) const senderFirstBalanceAfter = new BigNumber(await firstOtoken.balanceOf(holder1)) const senderSecondBalanceAfter = new BigNumber(await secondOtoken.balanceOf(holder1)) @@ -3241,6 +3380,7 @@ contract( // open new vault, mint naked short, sell it to holder 1 const collateralToDespoit = new BigNumber(await shortOtoken.strikePrice()).dividedBy(100) const vaultCounter = new BigNumber(await controllerProxy.getAccountVaultCounter(accountOwner1)).plus(1) + const actionArgs = [ { actionType: ActionType.OpenVault, @@ -3263,7 +3403,10 @@ contract( data: ZERO_ADDR, }, ] - await usdc.approve(marginPool.address, collateralToDespoit, { from: accountOwner1 }) + const finalMarginPool = (await borrowableMarginPool.isWhitelistedOptionsVault(accountOwner1)) + ? borrowableMarginPool.address + : marginPool.address + await usdc.approve(finalMarginPool, collateralToDespoit, { from: accountOwner1 }) await controllerProxy.operate(actionArgs, { from: accountOwner1 }) }) @@ -3382,6 +3525,10 @@ contract( await oracle.setExpiryPriceFinalizedAllPeiodOver(weth.address, expiry, createTokenAmount(150), true) await oracle.setExpiryPriceFinalizedAllPeiodOver(usdc.address, expiry, createTokenAmount(1), true) const vaultCounter = new BigNumber(await controllerProxy.getAccountVaultCounter(accountOwner1)) + const finalMarginPool = (await borrowableMarginPool.isWhitelistedOptionsVault(accountOwner1)) + ? borrowableMarginPool.address + : marginPool.address + const actionArgs = [ { actionType: ActionType.SettleVault, @@ -3396,7 +3543,7 @@ contract( ] const payout = createTokenAmount(150, usdcDecimals) - const marginPoolBalanceBefore = new BigNumber(await usdc.balanceOf(marginPool.address)) + const marginPoolBalanceBefore = new BigNumber(await usdc.balanceOf(finalMarginPool)) const senderBalanceBefore = new BigNumber(await usdc.balanceOf(accountOwner1)) const proceed = await controllerProxy.getProceed(accountOwner1, vaultCounter) @@ -3404,7 +3551,7 @@ contract( await controllerProxy.operate(actionArgs, { from: accountOwner1 }) - const marginPoolBalanceAfter = new BigNumber(await usdc.balanceOf(marginPool.address)) + const marginPoolBalanceAfter = new BigNumber(await usdc.balanceOf(finalMarginPool)) const senderBalanceAfter = new BigNumber(await usdc.balanceOf(accountOwner1)) assert.equal( @@ -3438,6 +3585,10 @@ contract( // mint some long otokens, (so we can put it as long) const vaultCounter = new BigNumber(await controllerProxy.getAccountVaultCounter(accountOwner1)).plus(1) + const finalMarginPool = (await borrowableMarginPool.isWhitelistedOptionsVault(accountOwner1)) + ? borrowableMarginPool.address + : marginPool.address + const longAmount = createTokenAmount(1) const collateralAmount = createTokenAmount(stirkePrice, usdcDecimals) const mintArgs = [ @@ -3472,11 +3623,10 @@ contract( data: ZERO_ADDR, }, ] - await usdc.approve(marginPool.address, collateralAmount, { from: accountOwner1 }) + await usdc.approve(finalMarginPool, collateralAmount, { from: accountOwner1 }) await controllerProxy.operate(mintArgs, { from: accountOwner1 }) - // Use the newly minted otoken as long and put it in a new vault - const newVulatId = vaultCounter.toNumber() + 1 + const newVaultId = vaultCounter.toNumber() + 1 const newVaultArgs = [ { @@ -3484,7 +3634,7 @@ contract( owner: accountOwner1, secondAddress: accountOwner1, asset: ZERO_ADDR, - vaultId: newVulatId, + vaultId: newVaultId, amount: '0', index: '0', data: ZERO_ADDR, @@ -3494,13 +3644,13 @@ contract( owner: accountOwner1, secondAddress: accountOwner1, asset: longOtoken.address, - vaultId: newVulatId, + vaultId: newVaultId, amount: longAmount, index: '0', data: ZERO_ADDR, }, ] - await longOtoken.approve(marginPool.address, longAmount, { from: accountOwner1 }) + await longOtoken.approve(finalMarginPool, longAmount, { from: accountOwner1 }) await whitelist.whitelistOtoken(longOtoken.address) await controllerProxy.operate(newVaultArgs, { from: accountOwner1 }) // go to expiry @@ -3520,7 +3670,7 @@ contract( owner: accountOwner1, secondAddress: accountOwner1, asset: ZERO_ADDR, - vaultId: newVulatId, + vaultId: newVaultId, amount: '0', index: '0', data: ZERO_ADDR, @@ -3528,15 +3678,14 @@ contract( ] const expectedPayout = new BigNumber(createTokenAmount(stirkePrice - ethPriceAtExpiry, usdcDecimals)) const ownerUSDCBalanceBefore = new BigNumber(await usdc.balanceOf(accountOwner1)) - const poolOtokenBefore = new BigNumber(await longOtoken.balanceOf(marginPool.address)) + const poolOtokenBefore = new BigNumber(await longOtoken.balanceOf(finalMarginPool)) - const amountPayout = await controllerProxy.getProceed(accountOwner1, newVulatId) + const amountPayout = await controllerProxy.getProceed(accountOwner1, newVaultId) assert.equal(amountPayout.toString(), expectedPayout.toString(), 'payout calculation mismatch') - await controllerProxy.operate(settleArgs, { from: accountOwner1 }) const ownerUSDCBalanceAfter = new BigNumber(await usdc.balanceOf(accountOwner1)) - const poolOtokenAfter = new BigNumber(await longOtoken.balanceOf(marginPool.address)) + const poolOtokenAfter = new BigNumber(await longOtoken.balanceOf(finalMarginPool)) assert.equal( ownerUSDCBalanceAfter.toString(), ownerUSDCBalanceBefore.plus(amountPayout).toString(), @@ -3572,6 +3721,10 @@ contract( let collateralToDespoit = createTokenAmount(200, usdcDecimals) let amountToMint = createTokenAmount(1) let vaultCounter = new BigNumber(await controllerProxy.getAccountVaultCounter(accountOwner1)).plus(1) + const finalMarginPool = (await borrowableMarginPool.isWhitelistedOptionsVault(accountOwner1)) + ? borrowableMarginPool.address + : marginPool.address + let actionArgs = [ { actionType: ActionType.OpenVault, @@ -3604,7 +3757,7 @@ contract( data: ZERO_ADDR, }, ] - await usdc.approve(marginPool.address, collateralToDespoit, { from: accountOwner1 }) + await usdc.approve(finalMarginPool, collateralToDespoit, { from: accountOwner1 }) await controllerProxy.operate(actionArgs, { from: accountOwner1 }) expiryTime = new BigNumber(60 * 60 * 24 * 2) // after 1 day @@ -3624,6 +3777,7 @@ contract( collateralToDespoit = createTokenAmount(200, usdcDecimals) amountToMint = createTokenAmount(1) vaultCounter = new BigNumber(await controllerProxy.getAccountVaultCounter(accountOwner1)).plus(1) + actionArgs = [ { actionType: ActionType.OpenVault, @@ -3656,7 +3810,7 @@ contract( data: ZERO_ADDR, }, ] - await usdc.approve(marginPool.address, collateralToDespoit, { from: accountOwner1 }) + await usdc.approve(finalMarginPool, collateralToDespoit, { from: accountOwner1 }) await controllerProxy.operate(actionArgs, { from: accountOwner1 }) }) @@ -3669,6 +3823,9 @@ contract( await oracle.setExpiryPriceFinalizedAllPeiodOver(weth.address, expiry2, createTokenAmount(200), true) await oracle.setExpiryPriceFinalizedAllPeiodOver(usdc.address, expiry2, createTokenAmount(1), true) const vaultCounter = new BigNumber(await controllerProxy.getAccountVaultCounter(accountOwner1)) + const finalMarginPool = (await borrowableMarginPool.isWhitelistedOptionsVault(accountOwner1)) + ? borrowableMarginPool.address + : marginPool.address const actionArgs = [ { actionType: ActionType.SettleVault, @@ -3693,12 +3850,12 @@ contract( ] const payout = createTokenAmount(400, usdcDecimals) - const marginPoolBalanceBefore = new BigNumber(await usdc.balanceOf(marginPool.address)) + const marginPoolBalanceBefore = new BigNumber(await usdc.balanceOf(finalMarginPool)) const senderBalanceBefore = new BigNumber(await usdc.balanceOf(accountOwner1)) controllerProxy.operate(actionArgs, { from: accountOwner1 }) - const marginPoolBalanceAfter = new BigNumber(await usdc.balanceOf(marginPool.address)) + const marginPoolBalanceAfter = new BigNumber(await usdc.balanceOf(finalMarginPool)) const senderBalanceAfter = new BigNumber(await usdc.balanceOf(accountOwner1)) assert.equal( @@ -3874,10 +4031,6 @@ contract( ) }) - it('should revert activating call action restriction when it is already activated', async () => { - await expectRevert(controllerProxy.setCallRestriction(true, { from: owner }), 'C9') - }) - it('should revert calling any arbitrary address when call restriction is activated', async () => { const arbitraryTarget: CallTesterInstance = await CallTester.new() @@ -3926,10 +4079,6 @@ contract( assert.equal(await controllerProxy.callRestricted(), false, 'Call action restriction deactivation failed') }) - - it('should revert deactivating call action restriction when it is already deactivated', async () => { - await expectRevert(controllerProxy.setCallRestriction(false, { from: owner }), 'C9') - }) }) describe('Sync vault latest update timestamp', () => { @@ -3974,6 +4123,10 @@ contract( // open new vault, mint naked short, sell it to holder 1 const collateralToDespoit = new BigNumber(await shortOtokenV1.strikePrice()).dividedBy(100) const vaultCounter = new BigNumber(await controllerProxy.getAccountVaultCounter(accountOwner1)).plus(1) + const finalMarginPool = (await borrowableMarginPool.isWhitelistedOptionsVault(accountOwner1)) + ? borrowableMarginPool.address + : marginPool.address + const amountToMint = createTokenAmount(1) const actionArgs = [ { @@ -4007,7 +4160,7 @@ contract( data: ZERO_ADDR, }, ] - await usdc.approve(marginPool.address, collateralToDespoit, { from: accountOwner1 }) + await usdc.approve(finalMarginPool, collateralToDespoit, { from: accountOwner1 }) await controllerProxy.operate(actionArgs, { from: accountOwner1 }) //transfer to holder @@ -4022,6 +4175,9 @@ contract( await oracle.setExpiryPriceFinalizedAllPeiodOver(weth.address, expiry, createTokenAmount(150), true) await oracle.setExpiryPriceFinalizedAllPeiodOver(usdc.address, expiry, createTokenAmount(1), true) const vaultCounter = new BigNumber(await controllerProxy.getAccountVaultCounter(accountOwner1)) + const finalMarginPool = (await borrowableMarginPool.isWhitelistedOptionsVault(accountOwner1)) + ? borrowableMarginPool.address + : marginPool.address const actionArgs = [ { actionType: ActionType.SettleVault, @@ -4036,7 +4192,7 @@ contract( ] const payout = createTokenAmount(150, usdcDecimals) - const marginPoolBalanceBefore = new BigNumber(await usdc.balanceOf(marginPool.address)) + const marginPoolBalanceBefore = new BigNumber(await usdc.balanceOf(finalMarginPool)) const senderBalanceBefore = new BigNumber(await usdc.balanceOf(accountOwner1)) const proceed = await controllerProxy.getProceed(accountOwner1, vaultCounter) @@ -4044,7 +4200,7 @@ contract( await controllerProxy.operate(actionArgs, { from: accountOwner1 }) - const marginPoolBalanceAfter = new BigNumber(await usdc.balanceOf(marginPool.address)) + const marginPoolBalanceAfter = new BigNumber(await usdc.balanceOf(finalMarginPool)) const senderBalanceAfter = new BigNumber(await usdc.balanceOf(accountOwner1)) assert.equal( @@ -4087,6 +4243,7 @@ contract( before(async () => { const vaultCounterBefore = new BigNumber(await controllerProxy.getAccountVaultCounter(accountOwner1)) + const expiryTime = new BigNumber(60 * 60) // after 1 hour shortOtoken = await MockOtoken.new() // init otoken @@ -4137,7 +4294,11 @@ contract( data: ZERO_ADDR, }, ] - await usdc.approve(marginPool.address, collateralToDeposit, { from: accountOwner1 }) + const finalMarginPool = (await borrowableMarginPool.isWhitelistedOptionsVault(accountOwner1)) + ? borrowableMarginPool.address + : marginPool.address + + await usdc.approve(finalMarginPool, collateralToDeposit, { from: accountOwner1 }) await controllerProxy.operate(actionArgs, { from: accountOwner1 }) }) @@ -4153,18 +4314,10 @@ contract( assert.equal(await controllerProxy.partialPauser(), partialPauser, 'pauser address mismatch') }) - it('should revert set pauser address to the same previous address', async () => { - await expectRevert(controllerProxy.setPartialPauser(partialPauser, { from: owner }), 'C9') - }) - it('should revert when pausing the system from address other than pauser', async () => { await expectRevert(controllerProxy.setSystemPartiallyPaused(true, { from: random }), 'C2') }) - it('should revert partially un-pausing an already running system', async () => { - await expectRevert(controllerProxy.setSystemPartiallyPaused(false, { from: partialPauser }), 'C9') - }) - it('should pause system', async () => { const stateBefore = await controllerProxy.systemPartiallyPaused() assert.equal(stateBefore, false, 'System already paused') @@ -4175,10 +4328,6 @@ contract( assert.equal(stateAfter, true, 'System not paused') }) - it('should revert partially pausing an already patially paused system', async () => { - await expectRevert(controllerProxy.setSystemPartiallyPaused(true, { from: partialPauser }), 'C9') - }) - it('should revert opening a vault when system is partially paused', async () => { const vaultCounter = new BigNumber(await controllerProxy.getAccountVaultCounter(accountOwner1)) const actionArgs = [ @@ -4198,6 +4347,9 @@ contract( it('should revert depositing collateral when system is partially paused', async () => { const vaultCounter = new BigNumber(await controllerProxy.getAccountVaultCounter(accountOwner1)) + const finalMarginPool = (await borrowableMarginPool.isWhitelistedOptionsVault(accountOwner1)) + ? borrowableMarginPool.address + : marginPool.address const collateralToDeposit = new BigNumber(await shortOtoken.strikePrice()).dividedBy(1e8) const actionArgs = [ { @@ -4211,12 +4363,15 @@ contract( data: ZERO_ADDR, }, ] - await usdc.approve(marginPool.address, collateralToDeposit, { from: accountOwner1 }) + await usdc.approve(finalMarginPool, collateralToDeposit, { from: accountOwner1 }) await expectRevert(controllerProxy.operate(actionArgs, { from: accountOwner1 }), 'C4') }) it('should revert minting short otoken when system is partially paused', async () => { const vaultCounter = new BigNumber(await controllerProxy.getAccountVaultCounter(accountOwner1)) + const finalMarginPool = (await borrowableMarginPool.isWhitelistedOptionsVault(accountOwner1)) + ? borrowableMarginPool.address + : marginPool.address const collateralToDeposit = new BigNumber(await shortOtoken.strikePrice()).dividedBy(1e8) const actionArgs = [ { @@ -4240,7 +4395,7 @@ contract( data: ZERO_ADDR, }, ] - await usdc.approve(marginPool.address, collateralToDeposit, { from: accountOwner1 }) + await usdc.approve(finalMarginPool, collateralToDeposit, { from: accountOwner1 }) await expectRevert(controllerProxy.operate(actionArgs, { from: accountOwner1 }), 'C4') }) @@ -4289,6 +4444,9 @@ contract( await oracle.setExpiryPriceFinalizedAllPeiodOver(usdc.address, expiry, createTokenAmount(1), true) const vaultCounter = new BigNumber(await controllerProxy.getAccountVaultCounter(accountOwner1)) + const finalMarginPool = (await borrowableMarginPool.isWhitelistedOptionsVault(accountOwner1)) + ? borrowableMarginPool.address + : marginPool.address const actionArgs = [ { actionType: ActionType.SettleVault, @@ -4303,12 +4461,12 @@ contract( ] const payout = createTokenAmount(150, usdcDecimals) - const marginPoolBalanceBefore = new BigNumber(await usdc.balanceOf(marginPool.address)) + const marginPoolBalanceBefore = new BigNumber(await usdc.balanceOf(finalMarginPool)) const senderBalanceBefore = new BigNumber(await usdc.balanceOf(accountOwner1)) await controllerProxy.operate(actionArgs, { from: accountOwner1 }) - const marginPoolBalanceAfter = new BigNumber(await usdc.balanceOf(marginPool.address)) + const marginPoolBalanceAfter = new BigNumber(await usdc.balanceOf(finalMarginPool)) const senderBalanceAfter = new BigNumber(await usdc.balanceOf(accountOwner1)) assert.equal( @@ -4328,6 +4486,10 @@ contract( // transfer to holder await shortOtoken.transfer(holder1, amountToRedeem, { from: accountOwner1 }) + const finalMarginPool = (await borrowableMarginPool.isWhitelistedOptionsVault(accountOwner1)) + ? borrowableMarginPool.address + : marginPool.address + const actionArgs = [ { actionType: ActionType.Redeem, @@ -4343,13 +4505,13 @@ contract( assert.equal(await controllerProxy.hasExpired(shortOtoken.address), true, 'Short otoken is not expired yet') const payout = createTokenAmount(50, usdcDecimals) - const marginPoolBalanceBefore = new BigNumber(await usdc.balanceOf(marginPool.address)) + const marginPoolBalanceBefore = new BigNumber(await usdc.balanceOf(finalMarginPool)) const senderBalanceBefore = new BigNumber(await usdc.balanceOf(holder1)) const senderShortBalanceBefore = new BigNumber(await shortOtoken.balanceOf(holder1)) await controllerProxy.operate(actionArgs, { from: holder1 }) - const marginPoolBalanceAfter = new BigNumber(await usdc.balanceOf(marginPool.address)) + const marginPoolBalanceAfter = new BigNumber(await usdc.balanceOf(finalMarginPool)) const senderBalanceAfter = new BigNumber(await usdc.balanceOf(holder1)) const senderShortBalanceAfter = new BigNumber(await shortOtoken.balanceOf(holder1)) @@ -4379,6 +4541,7 @@ contract( await controllerProxy.setSystemPartiallyPaused(false, { from: partialPauser }) const vaultCounterBefore = new BigNumber(await controllerProxy.getAccountVaultCounter(accountOwner1)) + const expiryTime = new BigNumber(60 * 60) // after 1 hour shortOtoken = await MockOtoken.new() // init otoken @@ -4429,7 +4592,11 @@ contract( data: ZERO_ADDR, }, ] - await usdc.approve(marginPool.address, collateralToDeposit, { from: accountOwner1 }) + const finalMarginPool = (await borrowableMarginPool.isWhitelistedOptionsVault(accountOwner1)) + ? borrowableMarginPool.address + : marginPool.address + + await usdc.approve(finalMarginPool, collateralToDeposit, { from: accountOwner1 }) await controllerProxy.operate(actionArgs, { from: accountOwner1 }) }) @@ -4440,10 +4607,6 @@ contract( ) }) - it('should revert set fullPauser address to address zero', async () => { - await expectRevert(controllerProxy.setFullPauser(ZERO_ADDR, { from: owner }), 'C10') - }) - it('should set fullPauser', async () => { await controllerProxy.setFullPauser(fullPauser, { from: owner }) assert.equal(await controllerProxy.fullPauser(), fullPauser, 'Full pauser wrong') @@ -4453,10 +4616,6 @@ contract( await expectRevert(controllerProxy.setSystemFullyPaused(true, { from: random }), 'C1') }) - it('should revert fully un-pausing an already running system', async () => { - await expectRevert(controllerProxy.setSystemFullyPaused(false, { from: fullPauser }), 'C9') - }) - it('should trigger full pause', async () => { const stateBefore = await controllerProxy.systemFullyPaused() assert.equal(stateBefore, false, 'System already in full pause state') @@ -4467,10 +4626,6 @@ contract( assert.equal(stateAfter, true, 'System not in full pause state') }) - it('should revert fully pausing an already fully paused system', async () => { - await expectRevert(controllerProxy.setSystemFullyPaused(true, { from: fullPauser }), 'C9') - }) - it('should revert opening a vault when system is in full pause state', async () => { const vaultCounter = new BigNumber(await controllerProxy.getAccountVaultCounter(accountOwner1)) const actionArgs = [ @@ -4490,6 +4645,9 @@ contract( it('should revert depositing collateral when system is in full pause state', async () => { const vaultCounter = new BigNumber(await controllerProxy.getAccountVaultCounter(accountOwner1)) + const finalMarginPool = (await borrowableMarginPool.isWhitelistedOptionsVault(accountOwner1)) + ? borrowableMarginPool.address + : marginPool.address const collateralToDeposit = new BigNumber(await shortOtoken.strikePrice()).dividedBy(1e8) const actionArgs = [ { @@ -4503,12 +4661,15 @@ contract( data: ZERO_ADDR, }, ] - await usdc.approve(marginPool.address, collateralToDeposit, { from: accountOwner1 }) + await usdc.approve(finalMarginPool, collateralToDeposit, { from: accountOwner1 }) await expectRevert(controllerProxy.operate(actionArgs, { from: accountOwner1 }), 'C5') }) it('should revert minting short otoken when system is in full pause state', async () => { const vaultCounter = new BigNumber(await controllerProxy.getAccountVaultCounter(accountOwner1)) + const finalMarginPool = (await borrowableMarginPool.isWhitelistedOptionsVault(accountOwner1)) + ? borrowableMarginPool.address + : marginPool.address const collateralToDeposit = new BigNumber(await shortOtoken.strikePrice()).dividedBy(1e8) const actionArgs = [ { @@ -4532,7 +4693,7 @@ contract( data: ZERO_ADDR, }, ] - await usdc.approve(marginPool.address, collateralToDeposit, { from: accountOwner1 }) + await usdc.approve(finalMarginPool, collateralToDeposit, { from: accountOwner1 }) await expectRevert(controllerProxy.operate(actionArgs, { from: accountOwner1 }), 'C5') }) @@ -4632,7 +4793,7 @@ contract( }) }) - describe('Donate to pool', () => { + describe('Donate to pool v1', () => { it('it should donate to margin pool', async () => { const amountToDonate = createTokenAmount(10, usdcDecimals) const storedBalanceBefore = new BigNumber(await marginPool.getStoredBalance(usdc.address)) @@ -4650,6 +4811,24 @@ contract( }) }) + describe('Donate to borrowable pool', () => { + it('it should donate to margin pool v2', async () => { + const amountToDonate = createTokenAmount(10, usdcDecimals) + const storedBalanceBefore = new BigNumber(await borrowableMarginPool.getStoredBalance(usdc.address)) + + await usdc.approve(borrowableMarginPool.address, amountToDonate, { from: donor }) + await controllerProxy.donateBorrowablePool(usdc.address, amountToDonate, { from: donor }) + + const storedBalanceAfter = new BigNumber(await borrowableMarginPool.getStoredBalance(usdc.address)) + + assert.equal( + storedBalanceAfter.minus(storedBalanceBefore).toString(), + amountToDonate, + 'Donated amount mismatch', + ) + }) + }) + describe('Refresh configuration', () => { it('should revert refreshing configuration from address other than owner', async () => { await expectRevert(controllerProxy.refreshConfiguration({ from: random }), 'Ownable: caller is not the owner') @@ -4669,17 +4848,23 @@ contract( // referesh controller configuration await controllerProxy.refreshConfiguration() - const [_whitelist, _oracle, _calculator, _pool] = await controllerProxy.getConfiguration() - assert.equal(_oracle, oracle.address, 'Oracle address mismatch after refresh') - assert.equal(_calculator, calculator.address, 'Calculator address mismatch after refresh') - assert.equal(_pool, marginPool.address, 'Oracle address mismatch after refresh') - assert.equal(_whitelist, whitelist.address, 'Oracle address mismatch after refresh') + assert.equal(await controllerProxy.oracle(), oracle.address, 'Oracle address mismatch after refresh') + assert.equal( + await controllerProxy.calculator(), + calculator.address, + 'Calculator address mismatch after refresh', + ) + assert.equal(await controllerProxy.pool(), marginPool.address, 'Oracle address mismatch after refresh') + assert.equal(await controllerProxy.whitelist(), whitelist.address, 'Oracle address mismatch after refresh') }) }) describe('Execute an invalid action', () => { it('Should execute transaction with no state updates', async () => { const vaultCounter = new BigNumber(await controllerProxy.getAccountVaultCounter(accountOwner1)) + const finalMarginPool = (await borrowableMarginPool.isWhitelistedOptionsVault(accountOwner1)) + ? borrowableMarginPool.address + : marginPool.address assert.isAbove(vaultCounter.toNumber(), 0, 'Account owner have no vault') const collateralToDeposit = createTokenAmount(10, usdcDecimals) @@ -4696,7 +4881,7 @@ contract( }, ] - await usdc.approve(marginPool.address, collateralToDeposit, { from: accountOwner1 }) + await usdc.approve(finalMarginPool, collateralToDeposit, { from: accountOwner1 }) await expectRevert.unspecified(controllerProxy.operate(actionArgs, { from: accountOwner1 })) }) }) diff --git a/test/unit-tests/controllerBorrowableMarginPool.test.ts b/test/unit-tests/controllerBorrowableMarginPool.test.ts new file mode 100644 index 000000000..2ec98555a --- /dev/null +++ b/test/unit-tests/controllerBorrowableMarginPool.test.ts @@ -0,0 +1,4889 @@ +import { + CallTesterInstance, + MarginCalculatorInstance, + MockOtokenInstance, + MockERC20Instance, + MockOracleInstance, + MockWhitelistModuleInstance, + MarginPoolInstance, + BorrowableMarginPoolInstance, + ControllerInstance, + AddressBookInstance, + OwnedUpgradeabilityProxyInstance, + OtokenImplV1Instance, +} from '../../build/types/truffle-types' +import BigNumber from 'bignumber.js' +import { createTokenAmount, createScaledNumber } from '../utils' + +const { expectRevert, expectEvent, time } = require('@openzeppelin/test-helpers') + +const CallTester = artifacts.require('CallTester.sol') +const MockERC20 = artifacts.require('MockERC20.sol') +const OtokenImplV1 = artifacts.require('OtokenImplV1.sol') +const MockOtoken = artifacts.require('MockOtoken.sol') +const MockOracle = artifacts.require('MockOracle.sol') +const OwnedUpgradeabilityProxy = artifacts.require('OwnedUpgradeabilityProxy.sol') +const MarginCalculator = artifacts.require('MarginCalculator.sol') +const MockWhitelistModule = artifacts.require('MockWhitelistModule.sol') +const AddressBook = artifacts.require('AddressBook.sol') +const MarginPool = artifacts.require('MarginPool.sol') +const BorrowableMarginPool = artifacts.require('BorrowableMarginPool.sol') +const Controller = artifacts.require('Controller.sol') +const MarginVault = artifacts.require('MarginVault.sol') + +// address(0) +const ZERO_ADDR = '0x0000000000000000000000000000000000000000' + +enum ActionType { + OpenVault, + MintShortOption, + BurnShortOption, + DepositLongOption, + WithdrawLongOption, + DepositCollateral, + WithdrawCollateral, + SettleVault, + Redeem, + Call, + InvalidAction, +} + +contract( + 'Controller', + ([owner, accountOwner1, accountOwner2, accountOperator1, holder1, partialPauser, fullPauser, random, donor]) => { + // ERC20 mock + let usdc: MockERC20Instance + let weth: MockERC20Instance + let weth2: MockERC20Instance + // Oracle module + let oracle: MockOracleInstance + // calculator module + let calculator: MarginCalculatorInstance + // margin pool module + let marginPool: MarginPoolInstance + // margin pool module v2 + let borrowableMarginPool: BorrowableMarginPoolInstance + // whitelist module mock + let whitelist: MockWhitelistModuleInstance + // addressbook module mock + let addressBook: AddressBookInstance + // controller module + let controllerImplementation: ControllerInstance + let controllerProxy: ControllerInstance + + const usdcDecimals = 6 + const wethDecimals = 18 + + before('Deployment', async () => { + // addressbook deployment + addressBook = await AddressBook.new() + // ERC20 deployment + usdc = await MockERC20.new('USDC', 'USDC', usdcDecimals) + weth = await MockERC20.new('WETH', 'WETH', wethDecimals) + weth2 = await MockERC20.new('WETH', 'WETH', wethDecimals) + // deploy Oracle module + oracle = await MockOracle.new(addressBook.address, { from: owner }) + // calculator deployment + calculator = await MarginCalculator.new(oracle.address) + // margin pool deployment + marginPool = await MarginPool.new(addressBook.address) + // margin pool v2 deployment + borrowableMarginPool = await BorrowableMarginPool.new(addressBook.address) + // whitelist module + whitelist = await MockWhitelistModule.new() + // set margin pool in addressbook + await addressBook.setMarginPool(marginPool.address) + // set borrowable margin pool in addressbook + await addressBook.setAddress(web3.utils.soliditySha3('BORROWABLE_POOL'), borrowableMarginPool.address, { + from: owner, + }) + // set calculator in addressbook + await addressBook.setMarginCalculator(calculator.address) + // set oracle in AddressBook + await addressBook.setOracle(oracle.address) + // set whitelist module address + await addressBook.setWhitelist(whitelist.address) + // deploy Controller module + const lib = await MarginVault.new() + await Controller.link('MarginVault', lib.address) + controllerImplementation = await Controller.new() + + // set controller address in AddressBook + await addressBook.setController(controllerImplementation.address, { from: owner }) + + // check controller deployment + const controllerProxyAddress = await addressBook.getController() + controllerProxy = await Controller.at(controllerProxyAddress) + const proxy: OwnedUpgradeabilityProxyInstance = await OwnedUpgradeabilityProxy.at(controllerProxyAddress) + + assert.equal(await proxy.proxyOwner(), addressBook.address, 'Proxy owner address mismatch') + assert.equal(await controllerProxy.owner(), owner, 'Controller owner address mismatch') + assert.equal(await controllerProxy.systemPartiallyPaused(), false, 'system is partially paused') + + // make everyone rich + await usdc.mint(accountOwner1, createTokenAmount(10000, usdcDecimals)) + await usdc.mint(accountOperator1, createTokenAmount(10000, usdcDecimals)) + await usdc.mint(random, createTokenAmount(10000, usdcDecimals)) + await usdc.mint(donor, createTokenAmount(10000, usdcDecimals)) + }) + + describe('Controller initialization', () => { + it('should revert when calling initialize if it is already initalized', async () => { + await expectRevert( + controllerProxy.initialize(addressBook.address, owner), + 'Contract instance has already been initialized', + ) + }) + + it('should revert when calling initialize with addressbook equal to zero', async () => { + const controllerImplementation = await Controller.new() + + await expectRevert(controllerImplementation.initialize(ZERO_ADDR, owner), 'C7') + }) + + it('should revert when calling initialize with owner equal to zero', async () => { + const controllerImplementation = await Controller.new() + + await expectRevert(controllerImplementation.initialize(addressBook.address, ZERO_ADDR), 'C8') + }) + }) + + describe('Account operator', () => { + it('should set operator', async () => { + assert.equal( + await controllerProxy.isOperator(accountOwner1, accountOperator1), + false, + 'Address is already an operator', + ) + + await controllerProxy.setOperator(accountOperator1, true, { from: accountOwner1 }) + + assert.equal( + await controllerProxy.isOperator(accountOwner1, accountOperator1), + true, + 'Operator address mismatch', + ) + }) + + it('should be able to remove operator', async () => { + await controllerProxy.setOperator(accountOperator1, false, { from: accountOwner1 }) + + assert.equal( + await controllerProxy.isOperator(accountOwner1, accountOperator1), + false, + 'Operator address mismatch', + ) + }) + }) + + describe('Vault', () => { + it('should get vault', async () => { + const vaultId = new BigNumber(0) + await controllerProxy.getVault(accountOwner1, vaultId) + }) + + it('should get vault balance', async () => { + const vaultId = new BigNumber(0) + const proceed = await controllerProxy.getProceed(accountOwner1, vaultId) + assert.equal(proceed.toString(), '0') + }) + }) + + describe('Open vault', () => { + it('should revert opening a vault an an account from random address', async () => { + const actionArgs = [ + { + actionType: ActionType.OpenVault, + owner: accountOwner1, + secondAddress: random, + asset: ZERO_ADDR, + vaultId: '1', + amount: '0', + index: '0', + data: ZERO_ADDR, + }, + ] + await expectRevert(controllerProxy.operate(actionArgs, { from: random }), 'C6') + }) + + it('should revert opening a vault a vault with id equal to zero', async () => { + const actionArgs = [ + { + actionType: ActionType.OpenVault, + owner: accountOwner1, + secondAddress: accountOwner1, + asset: ZERO_ADDR, + vaultId: '0', + amount: '0', + index: '0', + data: ZERO_ADDR, + }, + ] + await expectRevert(controllerProxy.operate(actionArgs, { from: accountOwner1 }), 'C15') + }) + + it('should revert opening multiple vaults in the same operate call', async () => { + const actionArgs = [ + { + actionType: ActionType.OpenVault, + owner: accountOwner1, + secondAddress: accountOwner1, + asset: ZERO_ADDR, + vaultId: '1', + amount: '0', + index: '0', + data: ZERO_ADDR, + }, + { + actionType: ActionType.OpenVault, + owner: accountOwner1, + secondAddress: accountOwner1, + asset: ZERO_ADDR, + vaultId: '2', + amount: '0', + index: '0', + data: ZERO_ADDR, + }, + ] + + await expectRevert(controllerProxy.operate(actionArgs, { from: accountOwner1 }), 'C13') + }) + + it('should revert opening a vault with vault type other than 0 or 1', async () => { + const invalidVault = web3.eth.abi.encodeParameter('uint256', 2) + + const actionArgs = [ + { + actionType: ActionType.OpenVault, + owner: accountOwner1, + secondAddress: accountOwner1, + asset: ZERO_ADDR, + vaultId: '1', + amount: '0', + index: '0', + data: invalidVault, + }, + ] + + await expectRevert(controllerProxy.operate(actionArgs, { from: accountOwner1 }), 'A3') + }) + + it('should revert opening multiple vaults for different owners in the same operate call', async () => { + await controllerProxy.setOperator(accountOwner1, true, { from: accountOwner2 }) + const actionArgs = [ + { + actionType: ActionType.OpenVault, + owner: accountOwner1, + secondAddress: accountOwner1, + asset: ZERO_ADDR, + vaultId: '1', + amount: '0', + index: '0', + data: ZERO_ADDR, + }, + { + actionType: ActionType.OpenVault, + owner: accountOwner2, + secondAddress: accountOwner1, + asset: ZERO_ADDR, + vaultId: '1', + amount: '0', + index: '0', + data: ZERO_ADDR, + }, + ] + + await expectRevert(controllerProxy.operate(actionArgs, { from: accountOwner1 }), 'C12') + }) + + it('should open vault', async () => { + const vaultCounterBefore = new BigNumber(await controllerProxy.getAccountVaultCounter(accountOwner1)) + assert.equal(vaultCounterBefore.toString(), '0', 'vault counter before mismatch') + + const finalMarginPool = (await borrowableMarginPool.isWhitelistedOptionsVault(accountOwner1)) + ? borrowableMarginPool.address + : marginPool.address + + const actionArgs = [ + { + actionType: ActionType.OpenVault, + owner: accountOwner1, + secondAddress: accountOwner1, + asset: ZERO_ADDR, + vaultId: vaultCounterBefore.toNumber() + 1, + amount: '0', + index: '0', + data: ZERO_ADDR, + }, + ] + await controllerProxy.operate(actionArgs, { from: accountOwner1 }) + + const vaultCounterAfter = new BigNumber(await controllerProxy.getAccountVaultCounter(accountOwner1)) + assert.equal(vaultCounterAfter.minus(vaultCounterBefore).toString(), '1', 'vault counter after mismatch') + }) + + it('should open vault from account operator', async () => { + await borrowableMarginPool.setOptionsVaultWhitelistedStatus(accountOwner1, true, { from: owner }) + await controllerProxy.setOperator(accountOperator1, true, { from: accountOwner1 }) + + const finalMarginPool = (await borrowableMarginPool.isWhitelistedOptionsVault(accountOwner1)) + ? borrowableMarginPool.address + : marginPool.address + + assert.equal( + await controllerProxy.isOperator(accountOwner1, accountOperator1), + true, + 'Operator address mismatch', + ) + + const vaultCounterBefore = new BigNumber(await controllerProxy.getAccountVaultCounter(accountOwner1)) + + const actionArgs = [ + { + actionType: ActionType.OpenVault, + owner: accountOwner1, + secondAddress: accountOperator1, + asset: ZERO_ADDR, + vaultId: vaultCounterBefore.toNumber() + 1, + amount: '0', + index: '0', + data: ZERO_ADDR, + }, + ] + await controllerProxy.operate(actionArgs, { from: accountOperator1 }) + + const vaultCounterAfter = new BigNumber(await controllerProxy.getAccountVaultCounter(accountOwner1)) + assert.equal(vaultCounterAfter.minus(vaultCounterBefore).toString(), '1', 'vault counter after mismatch') + assert.equal(finalMarginPool, borrowableMarginPool.address, 'vault margin pool mismatch') + }) + }) + + describe('Long otoken', () => { + let longOtoken: MockOtokenInstance + + before(async () => { + const expiryTime = new BigNumber(60 * 60 * 24) // after 1 day + + longOtoken = await MockOtoken.new() + // init otoken + await longOtoken.init( + addressBook.address, + weth.address, + usdc.address, + usdc.address, + createTokenAmount(200), + new BigNumber(await time.latest()).plus(expiryTime), + true, + ) + + await longOtoken.mintOtoken(accountOwner1, createTokenAmount(100)) + await longOtoken.mintOtoken(accountOperator1, createTokenAmount(100)) + }) + + describe('deposit long otoken', () => { + it('should revert depositing a non-whitelisted long otoken into vault', async () => { + const vaultCounter = new BigNumber(await controllerProxy.getAccountVaultCounter(accountOwner1)) + const finalMarginPool = (await borrowableMarginPool.isWhitelistedOptionsVault(accountOwner1)) + ? borrowableMarginPool.address + : marginPool.address + const longToDeposit = createTokenAmount(20) + const actionArgs = [ + { + actionType: ActionType.DepositLongOption, + owner: accountOwner1, + secondAddress: accountOwner1, + asset: longOtoken.address, + vaultId: vaultCounter.toNumber(), + amount: longToDeposit, + index: '0', + data: ZERO_ADDR, + }, + ] + + await longOtoken.approve(finalMarginPool, longToDeposit, { from: accountOwner1 }) + await expectRevert(controllerProxy.operate(actionArgs, { from: accountOwner1 }), 'C17') + }) + + it('should revert depositing long with invalid vault id', async () => { + // whitelist otoken + await whitelist.whitelistOtoken(longOtoken.address) + + const vaultCounter = new BigNumber('100') + + const finalMarginPool = (await borrowableMarginPool.isWhitelistedOptionsVault(accountOwner1)) + ? borrowableMarginPool.address + : marginPool.address + + const longToDeposit = createTokenAmount(20) + const actionArgs = [ + { + actionType: ActionType.DepositLongOption, + owner: accountOwner1, + secondAddress: accountOwner1, + asset: longOtoken.address, + vaultId: vaultCounter.toNumber(), + amount: longToDeposit, + index: '0', + data: ZERO_ADDR, + }, + ] + + await longOtoken.approve(finalMarginPool, longToDeposit, { from: accountOwner1 }) + await expectRevert(controllerProxy.operate(actionArgs, { from: accountOwner1 }), 'C35') + }) + + it('should revert depositing long from an address that is not the msg.sender nor the owner account address', async () => { + const vaultCounter = new BigNumber(await controllerProxy.getAccountVaultCounter(accountOwner1)) + const finalMarginPool = (await borrowableMarginPool.isWhitelistedOptionsVault(accountOwner1)) + ? borrowableMarginPool.address + : marginPool.address + const longToDeposit = createTokenAmount(20) + const actionArgs = [ + { + actionType: ActionType.DepositLongOption, + owner: accountOwner1, + secondAddress: random, + asset: longOtoken.address, + vaultId: vaultCounter.toNumber(), + amount: longToDeposit, + index: '0', + data: ZERO_ADDR, + }, + ] + + await longOtoken.approve(finalMarginPool, longToDeposit, { from: random }) + await longOtoken.approve(finalMarginPool, longToDeposit, { from: accountOperator1 }) + await expectRevert(controllerProxy.operate(actionArgs, { from: accountOperator1 }), 'C16') + }) + + it('should deposit long otoken into vault from account owner', async () => { + const vaultCounter = new BigNumber(await controllerProxy.getAccountVaultCounter(accountOwner1)) + + const finalMarginPool = (await borrowableMarginPool.isWhitelistedOptionsVault(accountOwner1)) + ? borrowableMarginPool.address + : marginPool.address + const longToDeposit = createTokenAmount(20) + const actionArgs = [ + { + actionType: ActionType.DepositLongOption, + owner: accountOwner1, + secondAddress: accountOwner1, + asset: longOtoken.address, + vaultId: vaultCounter.toNumber(), + amount: longToDeposit, + index: '0', + data: ZERO_ADDR, + }, + ] + const marginPoolBalanceBefore = new BigNumber(await longOtoken.balanceOf(finalMarginPool)) + const senderBalanceBefore = new BigNumber(await longOtoken.balanceOf(accountOwner1)) + + await longOtoken.approve(finalMarginPool, longToDeposit, { from: accountOwner1 }) + await controllerProxy.operate(actionArgs, { from: accountOwner1 }) + + const marginPoolBalanceAfter = new BigNumber(await longOtoken.balanceOf(finalMarginPool)) + const senderBalanceAfter = new BigNumber(await longOtoken.balanceOf(accountOwner1)) + const vaultAfter = await controllerProxy.getVault(accountOwner1, vaultCounter) + + assert.equal( + marginPoolBalanceAfter.minus(marginPoolBalanceBefore).toString(), + longToDeposit, + 'Margin pool balance long otoken balance mismatch', + ) + assert.equal( + senderBalanceBefore.minus(senderBalanceAfter).toString(), + longToDeposit, + 'Sender balance long otoken balance mismatch', + ) + assert.equal(vaultAfter.longOtokens.length, 1, 'Vault long otoken array length mismatch') + assert.equal( + vaultAfter.longOtokens[0], + longOtoken.address, + 'Long otoken address deposited into vault mismatch', + ) + assert.equal( + new BigNumber(vaultAfter.longAmounts[0]).toString(), + longToDeposit, + 'Long otoken amount deposited into vault mismatch', + ) + }) + + it('should deposit long otoken into vault from account operator', async () => { + assert.equal( + await controllerProxy.isOperator(accountOwner1, accountOperator1), + true, + 'Operator address mismatch', + ) + + const vaultCounter = new BigNumber(await controllerProxy.getAccountVaultCounter(accountOwner1)) + + const finalMarginPool = (await borrowableMarginPool.isWhitelistedOptionsVault(accountOwner1)) + ? borrowableMarginPool.address + : marginPool.address + const longToDeposit = createTokenAmount(20) + const actionArgs = [ + { + actionType: ActionType.DepositLongOption, + owner: accountOwner1, + secondAddress: accountOperator1, + asset: longOtoken.address, + vaultId: vaultCounter.toNumber(), + amount: longToDeposit, + index: '0', + data: ZERO_ADDR, + }, + ] + const marginPoolBalanceBefore = new BigNumber(await longOtoken.balanceOf(finalMarginPool)) + const senderBalanceBefore = new BigNumber(await longOtoken.balanceOf(accountOperator1)) + const vaultBefore = await controllerProxy.getVault(accountOwner1, vaultCounter) + + await longOtoken.approve(finalMarginPool, longToDeposit, { from: accountOperator1 }) + await controllerProxy.operate(actionArgs, { from: accountOperator1 }) + + const marginPoolBalanceAfter = new BigNumber(await longOtoken.balanceOf(finalMarginPool)) + const senderBalanceAfter = new BigNumber(await longOtoken.balanceOf(accountOperator1)) + const vaultAfter = await controllerProxy.getVault(accountOwner1, vaultCounter) + + assert.equal( + marginPoolBalanceAfter.minus(marginPoolBalanceBefore).toString(), + longToDeposit.toString(), + 'Margin pool balance long otoken balance mismatch', + ) + assert.equal( + senderBalanceBefore.minus(senderBalanceAfter).toString(), + longToDeposit.toString(), + 'Sender balance long otoken balance mismatch', + ) + assert.equal(vaultAfter.longOtokens.length, 1, 'Vault long otoken array length mismatch') + assert.equal( + vaultAfter.longOtokens[0], + longOtoken.address, + 'Long otoken address deposited into vault mismatch', + ) + assert.equal( + new BigNumber(vaultAfter.longAmounts[0]).minus(new BigNumber(vaultBefore.longAmounts[0])).toString(), + longToDeposit.toString(), + 'Long otoken amount deposited into vault mismatch', + ) + }) + + it('should execute depositing long otoken into vault in multiple actions', async () => { + const vaultCounter = new BigNumber(await controllerProxy.getAccountVaultCounter(accountOwner1)) + + const finalMarginPool = (await borrowableMarginPool.isWhitelistedOptionsVault(accountOwner1)) + ? borrowableMarginPool.address + : marginPool.address + const longToDeposit = new BigNumber(createTokenAmount(20)) + const actionArgs = [ + { + actionType: ActionType.DepositLongOption, + owner: accountOwner1, + secondAddress: accountOwner1, + asset: longOtoken.address, + vaultId: vaultCounter.toNumber(), + amount: longToDeposit.toString(), + index: '0', + data: ZERO_ADDR, + }, + { + actionType: ActionType.DepositLongOption, + owner: accountOwner1, + secondAddress: accountOwner1, + asset: longOtoken.address, + vaultId: vaultCounter.toNumber(), + amount: longToDeposit.toString(), + index: '0', + data: ZERO_ADDR, + }, + ] + const marginPoolBalanceBefore = new BigNumber(await longOtoken.balanceOf(finalMarginPool)) + const senderBalanceBefore = new BigNumber(await longOtoken.balanceOf(accountOwner1)) + const vaultBefore = await controllerProxy.getVault(accountOwner1, vaultCounter) + + await longOtoken.approve(finalMarginPool, longToDeposit.multipliedBy(2).toString(), { + from: accountOwner1, + }) + await controllerProxy.operate(actionArgs, { from: accountOwner1 }) + + const marginPoolBalanceAfter = new BigNumber(await longOtoken.balanceOf(finalMarginPool)) + const senderBalanceAfter = new BigNumber(await longOtoken.balanceOf(accountOwner1)) + const vaultAfter = await controllerProxy.getVault(accountOwner1, vaultCounter) + + assert.equal( + marginPoolBalanceAfter.minus(marginPoolBalanceBefore).toString(), + longToDeposit.multipliedBy(2).toString(), + 'Margin pool balance long otoken balance mismatch', + ) + assert.equal( + senderBalanceBefore.minus(senderBalanceAfter).toString(), + longToDeposit.multipliedBy(2).toString(), + 'Sender balance long otoken balance mismatch', + ) + assert.equal(vaultAfter.longOtokens.length, 1, 'Vault long otoken array length mismatch') + assert.equal( + vaultAfter.longOtokens[0], + longOtoken.address, + 'Long otoken address deposited into vault mismatch', + ) + assert.equal( + new BigNumber(vaultAfter.longAmounts[0]).minus(new BigNumber(vaultBefore.longAmounts[0])).toString(), + longToDeposit.multipliedBy(2).toString(), + 'Long otoken amount deposited into vault mismatch', + ) + }) + + it('should revert depositing long otoken with amount equal to zero', async () => { + const vaultCounter = new BigNumber(await controllerProxy.getAccountVaultCounter(accountOwner1)) + const finalMarginPool = (await borrowableMarginPool.isWhitelistedOptionsVault(accountOwner1)) + ? borrowableMarginPool.address + : marginPool.address + const longToDeposit = createTokenAmount(20) + const actionArgs = [ + { + actionType: ActionType.DepositLongOption, + owner: accountOwner1, + secondAddress: accountOwner1, + asset: longOtoken.address, + vaultId: vaultCounter.toNumber(), + amount: '0', + index: '0', + data: ZERO_ADDR, + }, + ] + + await longOtoken.approve(finalMarginPool, longToDeposit, { from: accountOwner1 }) + await expectRevert(controllerProxy.operate(actionArgs, { from: accountOwner1 }), 'V4') + }) + + it('should revert depositing an expired long otoken', async () => { + // deploy expired Otoken + const expiredLongOtoken: MockOtokenInstance = await MockOtoken.new() + // init otoken + await expiredLongOtoken.init( + addressBook.address, + weth.address, + usdc.address, + usdc.address, + createTokenAmount(200), + '1219926985', // 2008 + true, + ) + await expiredLongOtoken.mintOtoken(accountOwner1, new BigNumber('100')) + + // whitelist otoken + await whitelist.whitelistOtoken(expiredLongOtoken.address) + + const vaultCounter = new BigNumber(await controllerProxy.getAccountVaultCounter(accountOwner1)) + + const finalMarginPool = (await borrowableMarginPool.isWhitelistedOptionsVault(accountOwner1)) + ? borrowableMarginPool.address + : marginPool.address + const longToDeposit = createTokenAmount(20) + const actionArgs = [ + { + actionType: ActionType.DepositLongOption, + owner: accountOwner1, + secondAddress: accountOwner1, + asset: expiredLongOtoken.address, + vaultId: vaultCounter.toNumber(), + amount: longToDeposit, + index: '0', + data: ZERO_ADDR, + }, + ] + + await expiredLongOtoken.approve(finalMarginPool, longToDeposit, { from: accountOwner1 }) + await expectRevert(controllerProxy.operate(actionArgs, { from: accountOwner1 }), 'C18') + }) + + it('should revert when vault have more than 1 long otoken', async () => { + const expiryTime = new BigNumber(60 * 60) // after 1 hour + const longToDeposit = createTokenAmount(20) + // deploy second Otoken + const secondLongOtoken: MockOtokenInstance = await MockOtoken.new() + // init otoken + await secondLongOtoken.init( + addressBook.address, + weth.address, + usdc.address, + usdc.address, + createTokenAmount(200), + new BigNumber(await time.latest()).plus(expiryTime), + true, + ) + await secondLongOtoken.mintOtoken(accountOwner1, longToDeposit) + // whitelist otoken + await whitelist.whitelistOtoken(secondLongOtoken.address) + const vaultCounter = new BigNumber(await controllerProxy.getAccountVaultCounter(accountOwner1)) + + const finalMarginPool = (await borrowableMarginPool.isWhitelistedOptionsVault(accountOwner1)) + ? borrowableMarginPool.address + : marginPool.address + const actionArgs = [ + { + actionType: ActionType.DepositLongOption, + owner: accountOwner1, + secondAddress: accountOwner1, + asset: secondLongOtoken.address, + vaultId: vaultCounter.toNumber(), + amount: longToDeposit, + index: '1', + data: ZERO_ADDR, + }, + ] + + await secondLongOtoken.approve(finalMarginPool, longToDeposit, { from: accountOwner1 }) + await expectRevert( + controllerProxy.operate(actionArgs, { from: accountOwner1 }), + 'MarginCalculator: Too many long otokens in the vault', + ) + }) + + it('should revert deposting long from controller implementation contract instead of the controller proxy', async () => { + await controllerImplementation.initialize(addressBook.address, owner) + + const longToDeposit = createTokenAmount(20) + const actionArgs = [ + { + actionType: ActionType.OpenVault, + owner: accountOwner1, + secondAddress: random, + asset: ZERO_ADDR, + vaultId: '1', + amount: '0', + index: '0', + data: ZERO_ADDR, + }, + { + actionType: ActionType.DepositLongOption, + owner: accountOwner1, + secondAddress: accountOwner1, + asset: longOtoken.address, + vaultId: 1, + amount: longToDeposit, + index: '0', + data: ZERO_ADDR, + }, + ] + + const finalMarginPool = (await borrowableMarginPool.isWhitelistedOptionsVault(accountOwner1)) + ? borrowableMarginPool.address + : marginPool.address + await longOtoken.approve(finalMarginPool, longToDeposit, { from: accountOwner1 }) + + await expectRevert( + controllerImplementation.operate(actionArgs, { from: accountOwner1 }), + 'MarginPool: Sender is not Controller', + ) + }) + }) + + describe('withdraw long otoken', () => { + it('should revert withdrawing long otoken with wrong index from a vault', async () => { + const vaultCounter = new BigNumber(await controllerProxy.getAccountVaultCounter(accountOwner1)) + + assert.isAbove(vaultCounter.toNumber(), 0, 'Account owner have no vault') + + const longToWithdraw = createTokenAmount(20) + const actionArgs = [ + { + actionType: ActionType.WithdrawLongOption, + owner: accountOwner1, + secondAddress: accountOwner1, + asset: longOtoken.address, + vaultId: vaultCounter.toNumber(), + amount: longToWithdraw.toString(), + index: '1', + data: ZERO_ADDR, + }, + ] + + await expectRevert(controllerProxy.operate(actionArgs, { from: accountOwner1 }), 'V5') + }) + + it('should revert withdrawing long otoken from random address other than account owner or operator', async () => { + const vaultCounter = new BigNumber(await controllerProxy.getAccountVaultCounter(accountOwner1)) + assert.isAbove(vaultCounter.toNumber(), 0, 'Account owner have no vault') + + const longToWithdraw = createTokenAmount(20) + const actionArgs = [ + { + actionType: ActionType.WithdrawLongOption, + owner: accountOwner1, + secondAddress: accountOwner1, + asset: longOtoken.address, + vaultId: vaultCounter.toNumber(), + amount: longToWithdraw, + index: '0', + data: ZERO_ADDR, + }, + ] + + await expectRevert(controllerProxy.operate(actionArgs, { from: random }), 'C6') + }) + + it('should revert withdrawing long otoken amount greater than the vault balance', async () => { + const vaultCounter = new BigNumber(await controllerProxy.getAccountVaultCounter(accountOwner1)) + assert.isAbove(vaultCounter.toNumber(), 0, 'Account owner have no vault') + + const vaultBefore = await controllerProxy.getVault(accountOwner1, vaultCounter) + const longToWithdraw = new BigNumber(vaultBefore.longAmounts[0]).plus(1) + const actionArgs = [ + { + actionType: ActionType.WithdrawLongOption, + owner: accountOwner1, + secondAddress: accountOwner1, + asset: longOtoken.address, + vaultId: vaultCounter.toNumber(), + amount: longToWithdraw.toString(), + index: '0', + data: ZERO_ADDR, + }, + ] + + await expectRevert( + controllerProxy.operate(actionArgs, { from: accountOwner1 }), + 'SafeMath: subtraction overflow', + ) + }) + + it('should revert withdrawing long with invalid vault id', async () => { + const vaultCounter = new BigNumber('100') + + const longToWithdraw = createTokenAmount(10) + const actionArgs = [ + { + actionType: ActionType.WithdrawLongOption, + owner: accountOwner1, + secondAddress: accountOwner1, + asset: longOtoken.address, + vaultId: vaultCounter.toNumber(), + amount: longToWithdraw, + index: '0', + data: ZERO_ADDR, + }, + ] + + await expectRevert(controllerProxy.operate(actionArgs, { from: accountOwner1 }), 'C35') + }) + + it('should withdraw long otoken to any random address where msg.sender is account owner', async () => { + const vaultCounter = new BigNumber(await controllerProxy.getAccountVaultCounter(accountOwner1)) + const finalMarginPool = (await borrowableMarginPool.isWhitelistedOptionsVault(accountOwner1)) + ? borrowableMarginPool.address + : marginPool.address + assert.isAbove(vaultCounter.toNumber(), 0, 'Account owner have no vault') + + const longToWithdraw = createTokenAmount(10) + const actionArgs = [ + { + actionType: ActionType.WithdrawLongOption, + owner: accountOwner1, + secondAddress: random, + asset: longOtoken.address, + vaultId: vaultCounter.toNumber(), + amount: longToWithdraw, + index: '0', + data: ZERO_ADDR, + }, + ] + const marginPoolBalanceBefore = new BigNumber(await longOtoken.balanceOf(finalMarginPool)) + const receiverBalanceBefore = new BigNumber(await longOtoken.balanceOf(random)) + const vaultBefore = await controllerProxy.getVault(accountOwner1, vaultCounter) + + await controllerProxy.operate(actionArgs, { from: accountOwner1 }) + + const marginPoolBalanceAfter = new BigNumber(await longOtoken.balanceOf(finalMarginPool)) + const receiverBalanceAfter = new BigNumber(await longOtoken.balanceOf(random)) + const vaultAfter = await controllerProxy.getVault(accountOwner1, vaultCounter) + + assert.equal( + marginPoolBalanceBefore.minus(marginPoolBalanceAfter).toString(), + longToWithdraw.toString(), + 'Margin pool balance long otoken balance mismatch', + ) + assert.equal( + receiverBalanceAfter.minus(receiverBalanceBefore).toString(), + longToWithdraw.toString(), + 'Receiver long otoken balance mismatch', + ) + assert.equal(vaultAfter.longOtokens.length, 1, 'Vault long otoken array length mismatch') + assert.equal( + new BigNumber(vaultBefore.longAmounts[0]).minus(new BigNumber(vaultAfter.longAmounts[0])).toString(), + longToWithdraw.toString(), + 'Long otoken amount in vault after withdraw mismatch', + ) + }) + + it('should withdraw long otoken to any random address where msg.sender is account operator', async () => { + assert.equal( + await controllerProxy.isOperator(accountOwner1, accountOperator1), + true, + 'Operator address mismatch', + ) + + const vaultCounter = new BigNumber(await controllerProxy.getAccountVaultCounter(accountOwner1)) + const finalMarginPool = (await borrowableMarginPool.isWhitelistedOptionsVault(accountOwner1)) + ? borrowableMarginPool.address + : marginPool.address + assert.isAbove(vaultCounter.toNumber(), 0, 'Account owner have no vault') + + const longToWithdraw = createTokenAmount(10) + const actionArgs = [ + { + actionType: ActionType.WithdrawLongOption, + owner: accountOwner1, + secondAddress: random, + asset: longOtoken.address, + vaultId: vaultCounter.toNumber(), + amount: longToWithdraw, + index: '0', + data: ZERO_ADDR, + }, + ] + const marginPoolBalanceBefore = new BigNumber(await longOtoken.balanceOf(finalMarginPool)) + const receiverBalanceBefore = new BigNumber(await longOtoken.balanceOf(random)) + const vaultBefore = await controllerProxy.getVault(accountOwner1, vaultCounter) + + await controllerProxy.operate(actionArgs, { from: accountOperator1 }) + + const marginPoolBalanceAfter = new BigNumber(await longOtoken.balanceOf(finalMarginPool)) + const receiverBalanceAfter = new BigNumber(await longOtoken.balanceOf(random)) + const vaultAfter = await controllerProxy.getVault(accountOwner1, vaultCounter) + + assert.equal( + marginPoolBalanceBefore.minus(marginPoolBalanceAfter).toString(), + longToWithdraw.toString(), + 'Margin pool balance long otoken balance mismatch', + ) + assert.equal( + receiverBalanceAfter.minus(receiverBalanceBefore).toString(), + longToWithdraw.toString(), + 'Receiver long otoken balance mismatch', + ) + assert.equal(vaultAfter.longOtokens.length, 1, 'Vault long otoken array length mismatch') + assert.equal( + new BigNumber(vaultBefore.longAmounts[0]).minus(new BigNumber(vaultAfter.longAmounts[0])).toString(), + longToWithdraw.toString(), + 'Long otoken amount in vault after withdraw mismatch', + ) + }) + + it('should execute withdrawing long otoken in mutliple actions', async () => { + const vaultCounter = new BigNumber(await controllerProxy.getAccountVaultCounter(accountOwner1)) + const finalMarginPool = (await borrowableMarginPool.isWhitelistedOptionsVault(accountOwner1)) + ? borrowableMarginPool.address + : marginPool.address + assert.isAbove(vaultCounter.toNumber(), 0, 'Account owner have no vault') + + const longToWithdraw = new BigNumber(createTokenAmount(10)) + const actionArgs = [ + { + actionType: ActionType.WithdrawLongOption, + owner: accountOwner1, + secondAddress: accountOwner1, + asset: longOtoken.address, + vaultId: vaultCounter.toNumber(), + amount: longToWithdraw.toString(), + index: '0', + data: ZERO_ADDR, + }, + { + actionType: ActionType.WithdrawLongOption, + owner: accountOwner1, + secondAddress: accountOwner1, + asset: longOtoken.address, + vaultId: vaultCounter.toNumber(), + amount: longToWithdraw.toString(), + index: '0', + data: ZERO_ADDR, + }, + ] + const marginPoolBalanceBefore = new BigNumber(await longOtoken.balanceOf(finalMarginPool)) + const receiverBalanceBefore = new BigNumber(await longOtoken.balanceOf(accountOwner1)) + const vaultBefore = await controllerProxy.getVault(accountOwner1, vaultCounter) + + await controllerProxy.operate(actionArgs, { from: accountOwner1 }) + + const marginPoolBalanceAfter = new BigNumber(await longOtoken.balanceOf(finalMarginPool)) + const receiverBalanceAfter = new BigNumber(await longOtoken.balanceOf(accountOwner1)) + const vaultAfter = await controllerProxy.getVault(accountOwner1, vaultCounter) + + assert.equal( + marginPoolBalanceBefore.minus(marginPoolBalanceAfter).toString(), + longToWithdraw.multipliedBy(2).toString(), + 'Margin pool balance long otoken balance mismatch', + ) + assert.equal( + receiverBalanceAfter.minus(receiverBalanceBefore).toString(), + longToWithdraw.multipliedBy(2).toString(), + 'Receiver long otoken balance mismatch', + ) + assert.equal(vaultAfter.longOtokens.length, 1, 'Vault long otoken array length mismatch') + assert.equal( + new BigNumber(vaultBefore.longAmounts[0]).minus(new BigNumber(vaultAfter.longAmounts[0])).toString(), + longToWithdraw.multipliedBy(2).toString(), + 'Long otoken amount in vault after withdraw mismatch', + ) + }) + + it('should remove otoken address from otoken array if amount is equal to zero after withdrawing', async () => { + const vaultCounter = new BigNumber(await controllerProxy.getAccountVaultCounter(accountOwner1)) + const finalMarginPool = (await borrowableMarginPool.isWhitelistedOptionsVault(accountOwner1)) + ? borrowableMarginPool.address + : marginPool.address + assert.isAbove(vaultCounter.toNumber(), 0, 'Account owner have no vault') + + const vaultBefore = await controllerProxy.getVault(accountOwner1, vaultCounter) + + const longToWithdraw = new BigNumber(vaultBefore.longAmounts[0]) + const actionArgs = [ + { + actionType: ActionType.WithdrawLongOption, + owner: accountOwner1, + secondAddress: accountOwner1, + asset: longOtoken.address, + vaultId: vaultCounter.toNumber(), + amount: longToWithdraw.toString(), + index: '0', + data: ZERO_ADDR, + }, + ] + const marginPoolBalanceBefore = new BigNumber(await longOtoken.balanceOf(finalMarginPool)) + const receiverBalanceBefore = new BigNumber(await longOtoken.balanceOf(accountOwner1)) + + await controllerProxy.operate(actionArgs, { from: accountOwner1 }) + + const marginPoolBalanceAfter = new BigNumber(await longOtoken.balanceOf(finalMarginPool)) + const receiverBalanceAfter = new BigNumber(await longOtoken.balanceOf(accountOwner1)) + const vaultAfter = await controllerProxy.getVault(accountOwner1, vaultCounter) + + assert.equal( + marginPoolBalanceBefore.minus(marginPoolBalanceAfter).toString(), + longToWithdraw.toString(), + 'Margin pool balance long otoken balance mismatch', + ) + assert.equal( + receiverBalanceAfter.minus(receiverBalanceBefore).toString(), + longToWithdraw.toString(), + 'Receiver long otoken balance mismatch', + ) + assert.equal(vaultAfter.longOtokens.length, 1, 'Vault long otoken array length mismatch') + assert.equal(vaultAfter.longOtokens[0], ZERO_ADDR, 'Vault long otoken address after clearing mismatch') + assert.equal( + new BigNumber(vaultBefore.longAmounts[0]).minus(new BigNumber(vaultAfter.longAmounts[0])).toString(), + longToWithdraw.toString(), + 'Long otoken amount in vault after withdraw mismatch', + ) + }) + + describe('withdraw expired long otoken', () => { + let expiredLongOtoken: MockOtokenInstance + + before(async () => { + const expiryTime = new BigNumber(60 * 60) // after 1 hour + expiredLongOtoken = await MockOtoken.new() + // init otoken + await expiredLongOtoken.init( + addressBook.address, + weth.address, + usdc.address, + usdc.address, + createTokenAmount(200), + new BigNumber(await time.latest()).plus(expiryTime), + true, + ) + // some free money for the account owner + const longToDeposit = createTokenAmount(100) + await expiredLongOtoken.mintOtoken(accountOwner1, longToDeposit) + // whitelist otoken + await whitelist.whitelistOtoken(expiredLongOtoken.address, { from: owner }) + // deposit long otoken into vault + const vaultId = new BigNumber('1') + + const finalMarginPool = + (await controllerProxy.getVaultWithDetails(accountOwner1, vaultId))[1].toNumber() < 2 + ? marginPool.address + : borrowableMarginPool.address + + const actionArgs = [ + { + actionType: ActionType.DepositLongOption, + owner: accountOwner1, + secondAddress: accountOwner1, + asset: expiredLongOtoken.address, + vaultId: vaultId.toNumber(), + amount: longToDeposit, + index: '0', + data: ZERO_ADDR, + }, + ] + await expiredLongOtoken.approve(finalMarginPool, longToDeposit, { from: accountOwner1 }) + await controllerProxy.operate(actionArgs, { from: accountOwner1 }) + const vaultAfter = await controllerProxy.getVault(accountOwner1, vaultId) + assert.equal(vaultAfter.longOtokens.length, 1, 'Vault long otoken array length mismatch') + assert.equal( + vaultAfter.longOtokens[0], + expiredLongOtoken.address, + 'Long otoken address deposited into vault mismatch', + ) + assert.equal( + new BigNumber(vaultAfter.longAmounts[0]).toString(), + longToDeposit.toString(), + 'Long otoken amount deposited into vault mismatch', + ) + }) + + it('should revert withdrawing an expired long otoken', async () => { + // increment time after expiredLongOtoken expiry + await time.increase(3601) // increase time with one hour in seconds + + const vaultId = new BigNumber('1') + const vault = await controllerProxy.getVault(accountOwner1, vaultId) + const longToWithdraw = new BigNumber(vault.longAmounts[0]) + const actionArgs = [ + { + actionType: ActionType.WithdrawLongOption, + owner: accountOwner1, + secondAddress: accountOwner1, + asset: expiredLongOtoken.address, + vaultId: vaultId.toNumber(), + amount: longToWithdraw.toString(), + index: '0', + data: ZERO_ADDR, + }, + ] + + assert.equal( + await controllerProxy.hasExpired(expiredLongOtoken.address), + true, + 'Long otoken is not expired yet', + ) + + await expectRevert(controllerProxy.operate(actionArgs, { from: accountOwner1 }), 'C19') + }) + }) + }) + }) + + describe('Collateral asset', () => { + describe('Deposit collateral asset', () => { + it('should deposit a whitelisted collateral asset from account owner', async () => { + // whitelist usdc + await whitelist.whitelistCollateral(usdc.address) + const vaultCounter = new BigNumber(await controllerProxy.getAccountVaultCounter(accountOwner1)) + const finalMarginPool = (await borrowableMarginPool.isWhitelistedOptionsVault(accountOwner1)) + ? borrowableMarginPool.address + : marginPool.address + assert.isAbove(vaultCounter.toNumber(), 0, 'Account owner have no vault') + + const collateralToDeposit = createTokenAmount(10, usdcDecimals) + const actionArgs = [ + { + actionType: ActionType.DepositCollateral, + owner: accountOwner1, + secondAddress: accountOwner1, + asset: usdc.address, + vaultId: vaultCounter.toNumber(), + amount: collateralToDeposit, + index: '0', + data: ZERO_ADDR, + }, + ] + + const marginPoolBalanceBefore = new BigNumber(await usdc.balanceOf(finalMarginPool)) + const senderBalanceBefore = new BigNumber(await usdc.balanceOf(accountOwner1)) + + await usdc.approve(finalMarginPool, collateralToDeposit, { from: accountOwner1 }) + await controllerProxy.operate(actionArgs, { from: accountOwner1 }) + + const marginPoolBalanceAfter = new BigNumber(await usdc.balanceOf(finalMarginPool)) + const senderBalanceAfter = new BigNumber(await usdc.balanceOf(accountOwner1)) + const vaultAfter = await controllerProxy.getVault(accountOwner1, vaultCounter) + + assert.equal( + marginPoolBalanceAfter.minus(marginPoolBalanceBefore).toString(), + collateralToDeposit.toString(), + 'Margin pool balance collateral asset balance mismatch', + ) + assert.equal( + senderBalanceBefore.minus(senderBalanceAfter).toString(), + collateralToDeposit.toString(), + 'Sender balance collateral asset balance mismatch', + ) + assert.equal(vaultAfter.collateralAssets.length, 1, 'Vault collateral assets array length mismatch') + assert.equal( + vaultAfter.collateralAssets[0], + usdc.address, + 'Collateral asset address deposited into vault mismatch', + ) + assert.equal( + new BigNumber(vaultAfter.collateralAmounts[0]).toString(), + collateralToDeposit.toString(), + 'Collateral asset amount deposited into vault mismatch', + ) + }) + + it('should deposit a whitelisted collateral asset from account operator', async () => { + const vaultCounter = new BigNumber(await controllerProxy.getAccountVaultCounter(accountOwner1)) + const finalMarginPool = (await borrowableMarginPool.isWhitelistedOptionsVault(accountOwner1)) + ? borrowableMarginPool.address + : marginPool.address + assert.isAbove(vaultCounter.toNumber(), 0, 'Account owner have no vault') + + const collateralToDeposit = createTokenAmount(10, usdcDecimals) + const actionArgs = [ + { + actionType: ActionType.DepositCollateral, + owner: accountOwner1, + secondAddress: accountOperator1, + asset: usdc.address, + vaultId: vaultCounter.toNumber(), + amount: collateralToDeposit, + index: '0', + data: ZERO_ADDR, + }, + ] + + const marginPoolBalanceBefore = new BigNumber(await usdc.balanceOf(finalMarginPool)) + const senderBalanceBefore = new BigNumber(await usdc.balanceOf(accountOperator1)) + const vaultBefore = await controllerProxy.getVault(accountOwner1, vaultCounter) + + await usdc.approve(finalMarginPool, collateralToDeposit, { from: accountOperator1 }) + await controllerProxy.operate(actionArgs, { from: accountOperator1 }) + + const marginPoolBalanceAfter = new BigNumber(await usdc.balanceOf(finalMarginPool)) + const senderBalanceAfter = new BigNumber(await usdc.balanceOf(accountOperator1)) + const vaultAfter = await controllerProxy.getVault(accountOwner1, vaultCounter) + + assert.equal( + marginPoolBalanceAfter.minus(marginPoolBalanceBefore).toString(), + collateralToDeposit.toString(), + 'Margin pool balance collateral asset balance mismatch', + ) + assert.equal( + senderBalanceBefore.minus(senderBalanceAfter).toString(), + collateralToDeposit.toString(), + 'Sender balance collateral asset balance mismatch', + ) + assert.equal(vaultAfter.collateralAssets.length, 1, 'Vault collateral assets array length mismatch') + assert.equal( + vaultAfter.collateralAssets[0], + usdc.address, + 'Collateral asset address deposited into vault mismatch', + ) + assert.equal( + new BigNumber(vaultAfter.collateralAmounts[0]) + .minus(new BigNumber(vaultBefore.collateralAmounts[0])) + .toString(), + collateralToDeposit.toString(), + 'Long otoken amount deposited into vault mismatch', + ) + }) + + it('should revert depositing collateral asset with invalid vault id', async () => { + const vaultCounter = new BigNumber('100') + + const collateralToDeposit = createTokenAmount(10, usdcDecimals) + const actionArgs = [ + { + actionType: ActionType.DepositCollateral, + owner: accountOwner1, + secondAddress: accountOwner1, + asset: usdc.address, + vaultId: vaultCounter.toNumber(), + amount: collateralToDeposit, + index: '0', + data: ZERO_ADDR, + }, + ] + + const finalMarginPool = (await borrowableMarginPool.isWhitelistedOptionsVault(accountOwner1)) + ? borrowableMarginPool.address + : marginPool.address + + await usdc.approve(finalMarginPool, collateralToDeposit, { from: accountOwner1 }) + await expectRevert(controllerProxy.operate(actionArgs, { from: accountOwner1 }), 'C35') + }) + + it('should revert depositing long from an address that is not the msg.sender nor the owner account address', async () => { + const vaultCounter = new BigNumber(await controllerProxy.getAccountVaultCounter(accountOwner1)) + const finalMarginPool = (await borrowableMarginPool.isWhitelistedOptionsVault(accountOwner1)) + ? borrowableMarginPool.address + : marginPool.address + const collateralToDeposit = createTokenAmount(10, usdcDecimals) + const actionArgs = [ + { + actionType: ActionType.DepositCollateral, + owner: accountOwner1, + secondAddress: random, + asset: usdc.address, + vaultId: vaultCounter.toNumber(), + amount: collateralToDeposit, + index: '0', + data: ZERO_ADDR, + }, + ] + + await usdc.approve(finalMarginPool, collateralToDeposit, { from: random }) + await usdc.approve(finalMarginPool, collateralToDeposit, { from: accountOperator1 }) + await expectRevert(controllerProxy.operate(actionArgs, { from: accountOperator1 }), 'C20') + }) + + it('should revert depositing a collateral asset with amount equal to zero', async () => { + const vaultCounter = new BigNumber(await controllerProxy.getAccountVaultCounter(accountOwner1)) + const finalMarginPool = (await borrowableMarginPool.isWhitelistedOptionsVault(accountOwner1)) + ? borrowableMarginPool.address + : marginPool.address + assert.isAbove(vaultCounter.toNumber(), 0, 'Account owner have no vault') + + const collateralToDeposit = createTokenAmount(0, usdcDecimals) + const actionArgs = [ + { + actionType: ActionType.DepositCollateral, + owner: accountOwner1, + secondAddress: accountOwner1, + asset: usdc.address, + vaultId: vaultCounter.toNumber(), + amount: collateralToDeposit, + index: '0', + data: ZERO_ADDR, + }, + ] + + await usdc.approve(finalMarginPool, collateralToDeposit, { from: accountOwner1 }) + await expectRevert(controllerProxy.operate(actionArgs, { from: accountOwner1 }), 'V7') + }) + + it('should execute depositing collateral into vault in multiple actions', async () => { + const vaultCounter = new BigNumber(await controllerProxy.getAccountVaultCounter(accountOwner1)) + const finalMarginPool = (await borrowableMarginPool.isWhitelistedOptionsVault(accountOwner1)) + ? borrowableMarginPool.address + : marginPool.address + const collateralToDeposit = new BigNumber(createTokenAmount(20, usdcDecimals)) + const actionArgs = [ + { + actionType: ActionType.DepositCollateral, + owner: accountOwner1, + secondAddress: accountOwner1, + asset: usdc.address, + vaultId: vaultCounter.toNumber(), + amount: collateralToDeposit.toString(), + index: '0', + data: ZERO_ADDR, + }, + { + actionType: ActionType.DepositCollateral, + owner: accountOwner1, + secondAddress: accountOwner1, + asset: usdc.address, + vaultId: vaultCounter.toNumber(), + amount: collateralToDeposit.toString(), + index: '0', + data: ZERO_ADDR, + }, + ] + const marginPoolBalanceBefore = new BigNumber(await usdc.balanceOf(finalMarginPool)) + const senderBalanceBefore = new BigNumber(await usdc.balanceOf(accountOwner1)) + const vaultBefore = await controllerProxy.getVault(accountOwner1, vaultCounter) + + await usdc.approve(finalMarginPool, collateralToDeposit.multipliedBy(2), { from: accountOwner1 }) + await controllerProxy.operate(actionArgs, { from: accountOwner1 }) + + const marginPoolBalanceAfter = new BigNumber(await usdc.balanceOf(finalMarginPool)) + const senderBalanceAfter = new BigNumber(await usdc.balanceOf(accountOwner1)) + const vaultAfter = await controllerProxy.getVault(accountOwner1, vaultCounter) + + assert.equal( + marginPoolBalanceAfter.minus(marginPoolBalanceBefore).toString(), + collateralToDeposit.multipliedBy(2).toString(), + 'Margin pool collateral balance mismatch', + ) + assert.equal( + senderBalanceBefore.minus(senderBalanceAfter).toString(), + collateralToDeposit.multipliedBy(2).toString(), + 'Sender collateral asset balance mismatch', + ) + assert.equal(vaultAfter.collateralAmounts.length, 1, 'Vault collateral asset array length mismatch') + assert.equal( + vaultAfter.collateralAssets[0], + usdc.address, + 'Collateral asset address deposited into vault mismatch', + ) + assert.equal( + new BigNumber(vaultAfter.collateralAmounts[0]) + .minus(new BigNumber(vaultBefore.collateralAmounts[0])) + .toString(), + collateralToDeposit.multipliedBy(2).toString(), + 'Collateral asset amount deposited into vault mismatch', + ) + }) + + describe('Deposit un-whitelisted collateral asset', () => { + it('should revert depositing a collateral asset that is not whitelisted', async () => { + // deploy a shitcoin + const trx: MockERC20Instance = await MockERC20.new('TRX', 'TRX', 18) + await trx.mint(accountOwner1, new BigNumber('1000')) + + const vaultCounter = new BigNumber(await controllerProxy.getAccountVaultCounter(accountOwner1)) + const finalMarginPool = (await borrowableMarginPool.isWhitelistedOptionsVault(accountOwner1)) + ? borrowableMarginPool.address + : marginPool.address + assert.isAbove(vaultCounter.toNumber(), 0, 'Account owner have no vault') + + const collateralDeposit = createTokenAmount(10, wethDecimals) + const actionArgs = [ + { + actionType: ActionType.DepositCollateral, + owner: accountOwner1, + secondAddress: accountOwner1, + asset: trx.address, + vaultId: vaultCounter.toNumber(), + amount: collateralDeposit, + index: '0', + data: ZERO_ADDR, + }, + ] + + await trx.approve(finalMarginPool, collateralDeposit, { from: accountOwner1 }) + await expectRevert(controllerProxy.operate(actionArgs, { from: accountOwner1 }), 'C21') + }) + }) + + it('should revert when vault have more than 1 collateral type', async () => { + const collateralToDeposit = createTokenAmount(10, wethDecimals) + //whitelist weth to use in this test + await whitelist.whitelistCollateral(weth.address) + await weth.mint(accountOwner1, collateralToDeposit) + + const vaultCounter = new BigNumber(await controllerProxy.getAccountVaultCounter(accountOwner1)) + const finalMarginPool = (await borrowableMarginPool.isWhitelistedOptionsVault(accountOwner1)) + ? borrowableMarginPool.address + : marginPool.address + const actionArgs = [ + { + actionType: ActionType.DepositCollateral, + owner: accountOwner1, + secondAddress: accountOwner1, + asset: weth.address, + vaultId: vaultCounter.toNumber(), + amount: collateralToDeposit, + index: '1', + data: ZERO_ADDR, + }, + ] + + await weth.approve(finalMarginPool, collateralToDeposit, { from: accountOwner1 }) + await expectRevert( + controllerProxy.operate(actionArgs, { from: accountOwner1 }), + 'MarginCalculator: Too many collateral assets in the vault', + ) + }) + }) + + describe('withdraw collateral', () => { + it('should revert withdrawing collateral asset with wrong index from a vault', async () => { + const vaultCounter = new BigNumber(await controllerProxy.getAccountVaultCounter(accountOwner1)) + assert.isAbove(vaultCounter.toNumber(), 0, 'Account owner have no vault') + + const collateralToWithdraw = createTokenAmount(20, usdcDecimals) + const actionArgs = [ + { + actionType: ActionType.WithdrawCollateral, + owner: accountOwner1, + secondAddress: accountOwner1, + asset: usdc.address, + vaultId: vaultCounter.toNumber(), + amount: collateralToWithdraw, + index: '1', + data: ZERO_ADDR, + }, + ] + + await expectRevert(controllerProxy.operate(actionArgs, { from: accountOwner1 }), 'V8') + }) + + it('should revert withdrawing collateral asset from an invalid id', async () => { + const vaultCounter = new BigNumber(await controllerProxy.getAccountVaultCounter(accountOwner1)) + assert.isAbove(vaultCounter.toNumber(), 0, 'Account owner have no vault') + + const collateralToWithdraw = createTokenAmount(20, usdcDecimals) + const actionArgs = [ + { + actionType: ActionType.WithdrawCollateral, + owner: accountOwner1, + secondAddress: accountOwner1, + asset: usdc.address, + vaultId: '1350', + amount: collateralToWithdraw, + index: '1', + data: ZERO_ADDR, + }, + ] + + await expectRevert(controllerProxy.operate(actionArgs, { from: accountOwner1 }), 'C35') + }) + + it('should revert withdrawing collateral asset amount greater than the vault balance', async () => { + const vaultCounter = new BigNumber(await controllerProxy.getAccountVaultCounter(accountOwner1)) + assert.isAbove(vaultCounter.toNumber(), 0, 'Account owner have no vault') + + const vaultBefore = await controllerProxy.getVault(accountOwner1, vaultCounter) + const collateralToWithdraw = new BigNumber(vaultBefore.collateralAmounts[0]).plus(1) + const actionArgs = [ + { + actionType: ActionType.WithdrawCollateral, + owner: accountOwner1, + secondAddress: accountOwner1, + asset: usdc.address, + vaultId: vaultCounter.toNumber(), + amount: collateralToWithdraw.toNumber(), + index: '0', + data: ZERO_ADDR, + }, + ] + + await expectRevert( + controllerProxy.operate(actionArgs, { from: accountOwner1 }), + 'SafeMath: subtraction overflow', + ) + }) + + it('should withdraw collateral to any random address where msg.sender is account owner', async () => { + const vaultCounter = new BigNumber(await controllerProxy.getAccountVaultCounter(accountOwner1)) + const finalMarginPool = (await borrowableMarginPool.isWhitelistedOptionsVault(accountOwner1)) + ? borrowableMarginPool.address + : marginPool.address + assert.isAbove(vaultCounter.toNumber(), 0, 'Account owner have no vault') + + const collateralToWithdraw = createTokenAmount(10, usdcDecimals) + const actionArgs = [ + { + actionType: ActionType.WithdrawCollateral, + owner: accountOwner1, + secondAddress: random, + asset: usdc.address, + vaultId: vaultCounter.toNumber(), + amount: collateralToWithdraw, + index: '0', + data: ZERO_ADDR, + }, + ] + const marginPoolBalanceBefore = new BigNumber(await usdc.balanceOf(finalMarginPool)) + const receiverBalanceBefore = new BigNumber(await usdc.balanceOf(random)) + const vaultBefore = await controllerProxy.getVault(accountOwner1, vaultCounter) + + await controllerProxy.operate(actionArgs, { from: accountOwner1 }) + + const marginPoolBalanceAfter = new BigNumber(await usdc.balanceOf(finalMarginPool)) + const receiverBalanceAfter = new BigNumber(await usdc.balanceOf(random)) + const vaultAfter = await controllerProxy.getVault(accountOwner1, vaultCounter) + + assert.equal( + marginPoolBalanceBefore.minus(marginPoolBalanceAfter).toString(), + collateralToWithdraw.toString(), + 'Margin pool collateral asset balance mismatch', + ) + assert.equal( + receiverBalanceAfter.minus(receiverBalanceBefore).toString(), + collateralToWithdraw.toString(), + 'Receiver collateral asset balance mismatch', + ) + assert.equal(vaultAfter.collateralAssets.length, 1, 'Vault collateral asset array length mismatch') + assert.equal( + new BigNumber(vaultBefore.collateralAmounts[0]) + .minus(new BigNumber(vaultAfter.collateralAmounts[0])) + .toString(), + collateralToWithdraw.toString(), + 'Collateral asset amount in vault after withdraw mismatch', + ) + }) + + it('should withdraw collateral asset to any random address where msg.sender is account operator', async () => { + assert.equal( + await controllerProxy.isOperator(accountOwner1, accountOperator1), + true, + 'Operator address mismatch', + ) + + const vaultCounter = new BigNumber(await controllerProxy.getAccountVaultCounter(accountOwner1)) + + const finalMarginPool = (await borrowableMarginPool.isWhitelistedOptionsVault(accountOwner1)) + ? borrowableMarginPool.address + : marginPool.address + assert.isAbove(vaultCounter.toNumber(), 0, 'Account owner have no vault') + + const collateralToWithdraw = createTokenAmount(10, usdcDecimals) + const actionArgs = [ + { + actionType: ActionType.WithdrawCollateral, + owner: accountOwner1, + secondAddress: random, + asset: usdc.address, + vaultId: vaultCounter.toNumber(), + amount: collateralToWithdraw, + index: '0', + data: ZERO_ADDR, + }, + ] + const marginPoolBalanceBefore = new BigNumber(await usdc.balanceOf(finalMarginPool)) + const receiverBalanceBefore = new BigNumber(await usdc.balanceOf(random)) + const vaultBefore = await controllerProxy.getVault(accountOwner1, vaultCounter) + + await controllerProxy.operate(actionArgs, { from: accountOperator1 }) + + const marginPoolBalanceAfter = new BigNumber(await usdc.balanceOf(finalMarginPool)) + const receiverBalanceAfter = new BigNumber(await usdc.balanceOf(random)) + const vaultAfter = await controllerProxy.getVault(accountOwner1, vaultCounter) + + assert.equal( + marginPoolBalanceBefore.minus(marginPoolBalanceAfter).toString(), + collateralToWithdraw.toString(), + 'Margin pool collateral asset balance mismatch', + ) + assert.equal( + receiverBalanceAfter.minus(receiverBalanceBefore).toString(), + collateralToWithdraw.toString(), + 'Receiver collateral asset balance mismatch', + ) + assert.equal(vaultAfter.collateralAssets.length, 1, 'Vault collateral asset array length mismatch') + assert.equal( + new BigNumber(vaultBefore.collateralAmounts[0]) + .minus(new BigNumber(vaultAfter.collateralAmounts[0])) + .toString(), + collateralToWithdraw.toString(), + 'Collateral asset amount in vault after withdraw mismatch', + ) + }) + + it('should execute withdrawing collateral asset in mutliple actions', async () => { + const vaultCounter = new BigNumber(await controllerProxy.getAccountVaultCounter(accountOwner1)) + const finalMarginPool = (await borrowableMarginPool.isWhitelistedOptionsVault(accountOwner1)) + ? borrowableMarginPool.address + : marginPool.address + assert.isAbove(vaultCounter.toNumber(), 0, 'Account owner have no vault') + + const collateralToWithdraw = new BigNumber(createTokenAmount(10, usdcDecimals)) + const actionArgs = [ + { + actionType: ActionType.WithdrawCollateral, + owner: accountOwner1, + secondAddress: accountOwner1, + asset: usdc.address, + vaultId: vaultCounter.toNumber(), + amount: collateralToWithdraw.toString(), + index: '0', + data: ZERO_ADDR, + }, + { + actionType: ActionType.WithdrawCollateral, + owner: accountOwner1, + secondAddress: accountOwner1, + asset: usdc.address, + vaultId: vaultCounter.toNumber(), + amount: collateralToWithdraw.toString(), + index: '0', + data: ZERO_ADDR, + }, + ] + const marginPoolBalanceBefore = new BigNumber(await usdc.balanceOf(finalMarginPool)) + const receiverBalanceBefore = new BigNumber(await usdc.balanceOf(accountOwner1)) + const vaultBefore = await controllerProxy.getVault(accountOwner1, vaultCounter) + + await controllerProxy.operate(actionArgs, { from: accountOwner1 }) + + const marginPoolBalanceAfter = new BigNumber(await usdc.balanceOf(finalMarginPool)) + const receiverBalanceAfter = new BigNumber(await usdc.balanceOf(accountOwner1)) + const vaultAfter = await controllerProxy.getVault(accountOwner1, vaultCounter) + + assert.equal( + marginPoolBalanceBefore.minus(marginPoolBalanceAfter).toString(), + collateralToWithdraw.multipliedBy(2).toString(), + 'Margin pool collateral asset balance mismatch', + ) + assert.equal( + receiverBalanceAfter.minus(receiverBalanceBefore).toString(), + collateralToWithdraw.multipliedBy(2).toString(), + 'Receiver collateral asset balance mismatch', + ) + assert.equal(vaultAfter.collateralAssets.length, 1, 'Vault long otoken array length mismatch') + assert.equal( + new BigNumber(vaultBefore.collateralAmounts[0]) + .minus(new BigNumber(vaultAfter.collateralAmounts[0])) + .toString(), + collateralToWithdraw.multipliedBy(2).toString(), + 'Collateral asset amount in vault after withdraw mismatch', + ) + }) + + it('should remove collateral asset address from collateral array if amount is equal to zero after withdrawing', async () => { + const vaultCounter = new BigNumber(await controllerProxy.getAccountVaultCounter(accountOwner1)) + const finalMarginPool = (await borrowableMarginPool.isWhitelistedOptionsVault(accountOwner1)) + ? borrowableMarginPool.address + : marginPool.address + assert.isAbove(vaultCounter.toNumber(), 0, 'Account owner have no vault') + + const vaultBefore = await controllerProxy.getVault(accountOwner1, vaultCounter) + + const collateralToWithdraw = new BigNumber(vaultBefore.collateralAmounts[0]) + const actionArgs = [ + { + actionType: ActionType.WithdrawCollateral, + owner: accountOwner1, + secondAddress: accountOwner1, + asset: usdc.address, + vaultId: vaultCounter.toNumber(), + amount: collateralToWithdraw.toNumber(), + index: '0', + data: ZERO_ADDR, + }, + ] + const marginPoolBalanceBefore = new BigNumber(await usdc.balanceOf(finalMarginPool)) + const receiverBalanceBefore = new BigNumber(await usdc.balanceOf(accountOwner1)) + + await controllerProxy.operate(actionArgs, { from: accountOwner1 }) + + const marginPoolBalanceAfter = new BigNumber(await usdc.balanceOf(finalMarginPool)) + const receiverBalanceAfter = new BigNumber(await usdc.balanceOf(accountOwner1)) + const vaultAfter = await controllerProxy.getVault(accountOwner1, vaultCounter) + + assert.equal( + marginPoolBalanceBefore.minus(marginPoolBalanceAfter).toString(), + collateralToWithdraw.toString(), + 'Margin pool balance long otoken balance mismatch', + ) + assert.equal( + receiverBalanceAfter.minus(receiverBalanceBefore).toString(), + collateralToWithdraw.toString(), + 'Receiver long otoken balance mismatch', + ) + assert.equal(vaultAfter.collateralAssets.length, 1, 'Vault collateral asset array length mismatch') + assert.equal( + vaultAfter.collateralAssets[0], + ZERO_ADDR, + 'Vault collater asset address after clearing mismatch', + ) + assert.equal( + new BigNumber(vaultBefore.collateralAmounts[0]) + .minus(new BigNumber(vaultAfter.collateralAmounts[0])) + .toString(), + collateralToWithdraw.toString(), + 'Collateral asset amount in vault after withdraw mismatch', + ) + }) + }) + }) + + describe('Short otoken', () => { + let longOtoken: MockOtokenInstance + let shortOtoken: MockOtokenInstance + + before(async () => { + const expiryTime = new BigNumber(60 * 60 * 24) // after 1 day + + longOtoken = await MockOtoken.new() + shortOtoken = await MockOtoken.new() + // init otoken + await longOtoken.init( + addressBook.address, + weth.address, + usdc.address, + usdc.address, + createTokenAmount(250), + new BigNumber(await time.latest()).plus(expiryTime), + true, + ) + await shortOtoken.init( + addressBook.address, + weth.address, + usdc.address, + usdc.address, + createTokenAmount(200), + new BigNumber(await time.latest()).plus(expiryTime), + true, + ) + + // whitelist short otoken to be used in the protocol + await whitelist.whitelistOtoken(shortOtoken.address, { from: owner }) + + // give free money + await longOtoken.mintOtoken(accountOwner1, new BigNumber('100')) + await longOtoken.mintOtoken(accountOperator1, new BigNumber('100')) + await usdc.mint(accountOwner1, new BigNumber('1000000')) + await usdc.mint(accountOperator1, new BigNumber('1000000')) + await usdc.mint(random, new BigNumber('1000000')) + }) + + describe('Mint short otoken', () => { + it('should revert minting from random address other than owner or operator', async () => { + const vaultCounter = new BigNumber(await controllerProxy.getAccountVaultCounter(accountOwner1)) + assert.isAbove(vaultCounter.toNumber(), 0, 'Account owner have no vault') + + const amountToMint = createTokenAmount(1) + const actionArgs = [ + { + actionType: ActionType.MintShortOption, + owner: accountOwner1, + secondAddress: random, + asset: shortOtoken.address, + vaultId: vaultCounter.toNumber(), + amount: amountToMint, + index: '0', + data: ZERO_ADDR, + }, + ] + + await expectRevert(controllerProxy.operate(actionArgs, { from: random }), 'C6') + }) + + it('should revert minting using un-marginable collateral asset', async () => { + const vaultCounter = new BigNumber(await controllerProxy.getAccountVaultCounter(accountOwner1)) + const finalMarginPool = (await borrowableMarginPool.isWhitelistedOptionsVault(accountOwner1)) + ? borrowableMarginPool.address + : marginPool.address + assert.isAbove(vaultCounter.toNumber(), 0, 'Account owner have no vault') + + const collateralToDeposit = new BigNumber(await shortOtoken.strikePrice()).dividedBy(1e8) + const amountToMint = createTokenAmount(1, wethDecimals) + const actionArgs = [ + { + actionType: ActionType.MintShortOption, + owner: accountOwner1, + secondAddress: accountOwner1, + asset: shortOtoken.address, + vaultId: vaultCounter.toNumber(), + amount: amountToMint, + index: '0', + data: ZERO_ADDR, + }, + { + actionType: ActionType.DepositCollateral, + owner: accountOwner1, + secondAddress: accountOwner1, + asset: weth.address, + vaultId: vaultCounter.toNumber(), + amount: collateralToDeposit.toString(), + index: '0', + data: ZERO_ADDR, + }, + ] + + // free money + await weth.mint(accountOwner1, collateralToDeposit) + + await weth.approve(finalMarginPool, collateralToDeposit, { from: accountOwner1 }) + await expectRevert( + controllerProxy.operate(actionArgs, { from: accountOwner1 }), + 'MarginCalculator: collateral asset not marginable for short asset', + ) + }) + + it('should revert minting short with invalid vault id', async () => { + const vaultCounter = new BigNumber('100') + + const amountToMint = createTokenAmount(1) + const actionArgs = [ + { + actionType: ActionType.MintShortOption, + owner: accountOwner1, + secondAddress: accountOwner1, + asset: shortOtoken.address, + vaultId: vaultCounter.toNumber(), + amount: amountToMint, + index: '0', + data: ZERO_ADDR, + }, + ] + + await expectRevert(controllerProxy.operate(actionArgs, { from: accountOwner1 }), 'C35') + }) + + it('mint naked short otoken from owner', async () => { + const vaultCounter = new BigNumber(await controllerProxy.getAccountVaultCounter(accountOwner1)) + const finalMarginPool = (await borrowableMarginPool.isWhitelistedOptionsVault(accountOwner1)) + ? borrowableMarginPool.address + : marginPool.address + assert.isAbove(vaultCounter.toNumber(), 0, 'Account owner have no vault') + + const collateralToDeposit = new BigNumber(await shortOtoken.strikePrice()).dividedBy(100) + const amountToMint = createTokenAmount(1) + const actionArgs = [ + { + actionType: ActionType.MintShortOption, + owner: accountOwner1, + secondAddress: accountOwner1, + asset: shortOtoken.address, + vaultId: vaultCounter.toNumber(), + amount: amountToMint, + index: '0', + data: ZERO_ADDR, + }, + { + actionType: ActionType.DepositCollateral, + owner: accountOwner1, + secondAddress: accountOwner1, + asset: usdc.address, + vaultId: vaultCounter.toNumber(), + amount: collateralToDeposit.toNumber(), + index: '0', + data: ZERO_ADDR, + }, + ] + + const marginPoolBalanceBefore = new BigNumber(await usdc.balanceOf(finalMarginPool)) + const senderBalanceBefore = new BigNumber(await usdc.balanceOf(accountOwner1)) + const senderShortBalanceBefore = new BigNumber(await shortOtoken.balanceOf(accountOwner1)) + const vaultBefore = await controllerProxy.getVault(accountOwner1, vaultCounter) + + await usdc.approve(finalMarginPool, collateralToDeposit, { from: accountOwner1 }) + await controllerProxy.operate(actionArgs, { from: accountOwner1 }) + + const marginPoolBalanceAfter = new BigNumber(await usdc.balanceOf(finalMarginPool)) + const senderBalanceAfter = new BigNumber(await usdc.balanceOf(accountOwner1)) + const senderShortBalanceAfter = new BigNumber(await shortOtoken.balanceOf(accountOwner1)) + const vaultAfter = await controllerProxy.getVault(accountOwner1, vaultCounter) + + assert.equal( + marginPoolBalanceAfter.minus(marginPoolBalanceBefore).toString(), + collateralToDeposit.toString(), + 'Margin pool collateral asset balance mismatch', + ) + assert.equal( + senderBalanceBefore.minus(senderBalanceAfter).toString(), + collateralToDeposit.toString(), + 'Sender collateral asset balance mismatch', + ) + assert.equal(vaultAfter.collateralAssets.length, 1, 'Vault collateral asset array length mismatch') + assert.equal(vaultAfter.shortOtokens.length, 1, 'Vault short otoken array length mismatch') + assert.equal( + vaultAfter.collateralAssets[0], + usdc.address, + 'Collateral asset address deposited into vault mismatch', + ) + assert.equal( + vaultAfter.shortOtokens[0], + shortOtoken.address, + 'Short otoken address deposited into vault mismatch', + ) + assert.equal( + senderShortBalanceAfter.minus(senderShortBalanceBefore).toString(), + amountToMint.toString(), + 'Short otoken amount minted mismatch', + ) + assert.equal( + new BigNumber(vaultAfter.collateralAmounts[0]) + .minus(new BigNumber(vaultBefore.collateralAmounts[0])) + .toString(), + collateralToDeposit.toString(), + 'Collateral asset amount deposited into vault mismatch', + ) + assert.equal( + new BigNumber(vaultAfter.shortAmounts[0]).toString(), + amountToMint.toString(), + 'Short otoken amount minted into vault mismatch', + ) + }) + + it('mint naked short otoken from operator', async () => { + const vaultCounter = new BigNumber(await controllerProxy.getAccountVaultCounter(accountOwner1)) + const finalMarginPool = (await borrowableMarginPool.isWhitelistedOptionsVault(accountOwner1)) + ? borrowableMarginPool.address + : marginPool.address + assert.isAbove(vaultCounter.toNumber(), 0, 'Account owner have no vault') + + const collateralToDeposit = new BigNumber(await shortOtoken.strikePrice()).dividedBy(100) + const amountToMint = createTokenAmount(1) + const actionArgs = [ + { + actionType: ActionType.MintShortOption, + owner: accountOwner1, + secondAddress: accountOperator1, + asset: shortOtoken.address, + vaultId: vaultCounter.toNumber(), + amount: amountToMint, + index: '0', + data: ZERO_ADDR, + }, + { + actionType: ActionType.DepositCollateral, + owner: accountOwner1, + secondAddress: accountOperator1, + asset: usdc.address, + vaultId: vaultCounter.toNumber(), + amount: collateralToDeposit.toNumber(), + index: '0', + data: ZERO_ADDR, + }, + ] + + const marginPoolBalanceBefore = new BigNumber(await usdc.balanceOf(finalMarginPool)) + const senderBalanceBefore = new BigNumber(await usdc.balanceOf(accountOperator1)) + const senderShortBalanceBefore = new BigNumber(await shortOtoken.balanceOf(accountOperator1)) + const vaultBefore = await controllerProxy.getVault(accountOwner1, vaultCounter) + + await usdc.approve(finalMarginPool, collateralToDeposit, { from: accountOperator1 }) + await controllerProxy.operate(actionArgs, { from: accountOperator1 }) + + const marginPoolBalanceAfter = new BigNumber(await usdc.balanceOf(finalMarginPool)) + const senderBalanceAfter = new BigNumber(await usdc.balanceOf(accountOperator1)) + const senderShortBalanceAfter = new BigNumber(await shortOtoken.balanceOf(accountOperator1)) + const vaultAfter = await controllerProxy.getVault(accountOwner1, vaultCounter) + + assert.equal( + marginPoolBalanceAfter.minus(marginPoolBalanceBefore).toString(), + collateralToDeposit.toString(), + 'Margin pool collateral asset balance mismatch', + ) + assert.equal( + senderBalanceBefore.minus(senderBalanceAfter).toString(), + collateralToDeposit.toString(), + 'Sender collateral asset balance mismatch', + ) + assert.equal(vaultAfter.collateralAssets.length, 1, 'Vault collateral asset array length mismatch') + assert.equal(vaultAfter.shortOtokens.length, 1, 'Vault short otoken array length mismatch') + assert.equal( + vaultAfter.collateralAssets[0], + usdc.address, + 'Collateral asset address deposited into vault mismatch', + ) + assert.equal( + vaultAfter.shortOtokens[0], + shortOtoken.address, + 'Short otoken address deposited into vault mismatch', + ) + assert.equal( + senderShortBalanceAfter.minus(senderShortBalanceBefore).toString(), + amountToMint.toString(), + 'Short otoken amount minted mismatch', + ) + assert.equal( + new BigNumber(vaultAfter.collateralAmounts[0]) + .minus(new BigNumber(vaultBefore.collateralAmounts[0])) + .toString(), + collateralToDeposit.toString(), + 'Collateral asset amount deposited into vault mismatch', + ) + assert.equal( + new BigNumber(vaultAfter.shortAmounts[0]).minus(new BigNumber(vaultBefore.shortAmounts[0])).toString(), + amountToMint.toString(), + 'Short otoken amount minted into vault mismatch', + ) + }) + + it('should revert withdrawing collateral from naked short position when net value is equal to zero', async () => { + const vaultCounter = new BigNumber(await controllerProxy.getAccountVaultCounter(accountOwner1)) + assert.isAbove(vaultCounter.toNumber(), 0, 'Account owner have no vault') + + const vaultBefore = await controllerProxy.getVaultWithDetails(accountOwner1, vaultCounter) + + const [netValue, isExcess] = await calculator.getExcessCollateral(vaultBefore[0], vaultBefore[1]) + + const proceed = await controllerProxy.getProceed(accountOwner1, vaultCounter) + assert.equal(netValue.toString(), proceed.toString()) + + assert.equal(netValue.toString(), '0', 'Position net value mistmatch') + assert.equal(isExcess, true, 'Position collateral excess mismatch') + + const collateralToWithdraw = new BigNumber(vaultBefore[0].collateralAmounts[0]) + const actionArgs = [ + { + actionType: ActionType.WithdrawCollateral, + owner: accountOwner1, + secondAddress: accountOwner1, + asset: usdc.address, + vaultId: vaultCounter.toNumber(), + amount: collateralToWithdraw.toNumber(), + index: '0', + data: ZERO_ADDR, + }, + ] + + await expectRevert(controllerProxy.operate(actionArgs, { from: accountOwner1 }), 'C14') + }) + + it('should withdraw exceeded collateral from naked short position when net value > 0 ', async () => { + const vaultCounter = new BigNumber(await controllerProxy.getAccountVaultCounter(accountOwner1)) + const finalMarginPool = (await borrowableMarginPool.isWhitelistedOptionsVault(accountOwner1)) + ? borrowableMarginPool.address + : marginPool.address + assert.isAbove(vaultCounter.toNumber(), 0, 'Account owner have no vault') + + // deposit more collateral + const excessCollateralToDeposit = createTokenAmount(50, usdcDecimals) + const firstActionArgs = [ + { + actionType: ActionType.DepositCollateral, + owner: accountOwner1, + secondAddress: accountOwner1, + asset: usdc.address, + vaultId: vaultCounter.toNumber(), + amount: excessCollateralToDeposit, + index: '0', + data: ZERO_ADDR, + }, + ] + await usdc.approve(finalMarginPool, excessCollateralToDeposit, { from: accountOwner1 }) + await controllerProxy.operate(firstActionArgs, { from: accountOwner1 }) + + const vaultBefore = await controllerProxy.getVaultWithDetails(accountOwner1, vaultCounter) + const marginPoolBalanceBefore = new BigNumber(await usdc.balanceOf(finalMarginPool)) + const withdrawerBalanceBefore = new BigNumber(await usdc.balanceOf(accountOwner1)) + + const [netValue, isExcess] = await calculator.getExcessCollateral(vaultBefore[0], vaultBefore[1]) + + const proceed = await controllerProxy.getProceed(accountOwner1, vaultCounter) + assert.equal(netValue.toString(), proceed.toString()) + + assert.equal(netValue.toString(), excessCollateralToDeposit.toString(), 'Position net value mistmatch') + assert.equal(isExcess, true, 'Position collateral excess mismatch') + + const secondActionArgs = [ + { + actionType: ActionType.WithdrawCollateral, + owner: accountOwner1, + secondAddress: accountOwner1, + asset: usdc.address, + vaultId: vaultCounter.toNumber(), + amount: excessCollateralToDeposit, + index: '0', + data: ZERO_ADDR, + }, + ] + + await controllerProxy.operate(secondActionArgs, { from: accountOwner1 }) + + const vaultAfter = await controllerProxy.getVault(accountOwner1, vaultCounter) + const marginPoolBalanceAfter = new BigNumber(await usdc.balanceOf(finalMarginPool)) + const withdrawerBalanceAfter = new BigNumber(await usdc.balanceOf(accountOwner1)) + + assert.equal( + marginPoolBalanceBefore.minus(marginPoolBalanceAfter).toString(), + excessCollateralToDeposit.toString(), + 'Margin pool collateral asset balance mismatch', + ) + assert.equal( + withdrawerBalanceAfter.minus(withdrawerBalanceBefore).toString(), + excessCollateralToDeposit.toString(), + 'Receiver collateral asset balance mismatch', + ) + assert.equal(vaultAfter.collateralAssets.length, 1, 'Vault collateral asset array length mismatch') + assert.equal( + new BigNumber(vaultBefore[0].collateralAmounts[0]) + .minus(new BigNumber(vaultAfter.collateralAmounts[0])) + .toString(), + excessCollateralToDeposit.toString(), + 'Collateral asset amount in vault after withdraw mismatch', + ) + }) + + it('should revert when vault have more than 1 short otoken', async () => { + const vaultCounter = new BigNumber(await controllerProxy.getAccountVaultCounter(accountOwner1)) + assert.isAbove(vaultCounter.toNumber(), 0, 'Account owner have no vault') + + await whitelist.whitelistOtoken(longOtoken.address, { from: owner }) + + const amountToMint = '1' + + const actionArgs = [ + { + actionType: ActionType.MintShortOption, + owner: accountOwner1, + secondAddress: accountOwner1, + asset: longOtoken.address, + vaultId: vaultCounter.toNumber(), + amount: amountToMint, + index: '1', + data: ZERO_ADDR, + }, + ] + + await expectRevert( + controllerProxy.operate(actionArgs, { from: accountOwner1 }), + 'MarginCalculator: Too many short otokens in the vault', + ) + }) + + describe('Mint un-whitelisted short otoken', () => { + it('should revert minting an otoken that is not whitelisted in Whitelist module', async () => { + const expiryTime = new BigNumber(60 * 60 * 24) // after 1 day + + const notWhitelistedShortOtoken: MockOtokenInstance = await MockOtoken.new() + await notWhitelistedShortOtoken.init( + addressBook.address, + weth.address, + usdc.address, + usdc.address, + createTokenAmount(200), + new BigNumber(await time.latest()).plus(expiryTime), + true, + ) + + const collateralToDeposit = new BigNumber(await notWhitelistedShortOtoken.strikePrice()).dividedBy(100) + const amountToMint = createTokenAmount(1) + const actionArgs = [ + { + actionType: ActionType.OpenVault, + owner: accountOperator1, + secondAddress: accountOperator1, + asset: ZERO_ADDR, + vaultId: '1', + amount: '0', + index: '0', + data: ZERO_ADDR, + }, + { + actionType: ActionType.MintShortOption, + owner: accountOperator1, + secondAddress: accountOperator1, + asset: notWhitelistedShortOtoken.address, + vaultId: '1', + amount: amountToMint, + index: '0', + data: ZERO_ADDR, + }, + { + actionType: ActionType.DepositCollateral, + owner: accountOperator1, + secondAddress: accountOperator1, + asset: usdc.address, + vaultId: '1', + amount: collateralToDeposit.toNumber(), + index: '0', + data: ZERO_ADDR, + }, + ] + + const finalMarginPool = (await borrowableMarginPool.isWhitelistedOptionsVault(accountOwner1)) + ? borrowableMarginPool.address + : marginPool.address + + await usdc.approve(finalMarginPool, collateralToDeposit, { from: accountOperator1 }) + await expectRevert(controllerProxy.operate(actionArgs, { from: accountOperator1 }), 'C23') + }) + }) + + describe('Mint negligible amount', () => { + let oneDollarPut: MockOtokenInstance + let smallestPut: MockOtokenInstance + before('create options with small strike price', async () => { + oneDollarPut = await MockOtoken.new() + smallestPut = await MockOtoken.new() + // init otoken + await oneDollarPut.init( + addressBook.address, + weth.address, + usdc.address, + usdc.address, + createTokenAmount(1), + new BigNumber(await time.latest()).plus(86400), + true, + ) + await smallestPut.init( + addressBook.address, + weth.address, + usdc.address, + usdc.address, + 1, + new BigNumber(await time.latest()).plus(86400), + true, + ) + await whitelist.whitelistOtoken(oneDollarPut.address) + await whitelist.whitelistOtoken(smallestPut.address) + }) + it('should revert if trying to mint 1 wei of oToken with strikePrice = 1 USD without putting collateral', async () => { + const vaultId = (await controllerProxy.getAccountVaultCounter(accountOwner2)).toNumber() + 1 + const actionArgs = [ + { + actionType: ActionType.OpenVault, + owner: accountOwner2, + secondAddress: accountOwner2, + asset: ZERO_ADDR, + vaultId: vaultId, + amount: '0', + index: '0', + data: ZERO_ADDR, + }, + { + actionType: ActionType.MintShortOption, + owner: accountOwner2, + secondAddress: accountOwner2, + asset: oneDollarPut.address, + vaultId: vaultId, + amount: '1', + index: '0', + data: ZERO_ADDR, + }, + ] + await expectRevert(controllerProxy.operate(actionArgs, { from: accountOwner2 }), 'C14') + }) + + it('should revert minting 1 wei of oToken with minimal strikePrice without putting collateral', async () => { + const vaultId = (await controllerProxy.getAccountVaultCounter(accountOwner2)).toNumber() + 1 + const actionArgs = [ + { + actionType: ActionType.OpenVault, + owner: accountOwner2, + secondAddress: accountOwner2, + asset: ZERO_ADDR, + vaultId: vaultId, + amount: '0', + index: '0', + data: ZERO_ADDR, + }, + { + actionType: ActionType.MintShortOption, + owner: accountOwner2, + secondAddress: accountOwner2, + asset: smallestPut.address, + vaultId: vaultId, + amount: '1', + index: '0', + data: ZERO_ADDR, + }, + ] + + await expectRevert(controllerProxy.operate(actionArgs, { from: accountOwner2 }), 'C14') + }) + }) + }) + + describe('Burn short otoken', () => { + it('should revert burning short otoken with wrong index from a vault', async () => { + const vaultCounter = new BigNumber(await controllerProxy.getAccountVaultCounter(accountOwner1)) + assert.isAbove(vaultCounter.toNumber(), 0, 'Account owner have no vault') + + const shortOtokenToBurn = await shortOtoken.balanceOf(accountOwner1) + const actionArgs = [ + { + actionType: ActionType.BurnShortOption, + owner: accountOwner1, + secondAddress: accountOwner1, + asset: shortOtoken.address, + vaultId: vaultCounter.toNumber(), + amount: shortOtokenToBurn.toString(), + index: '1', + data: ZERO_ADDR, + }, + ] + + await expectRevert(controllerProxy.operate(actionArgs, { from: accountOwner1 }), 'V2') + }) + + it('should revert burning when there is no enough balance', async () => { + // transfer operator balance + const operatorShortBalance = new BigNumber(await shortOtoken.balanceOf(accountOperator1)) + await shortOtoken.transfer(accountOwner1, operatorShortBalance, { from: accountOperator1 }) + + const vaultCounter = new BigNumber(await controllerProxy.getAccountVaultCounter(accountOwner1)) + assert.isAbove(vaultCounter.toNumber(), 0, 'Account owner have no vault') + + const shortOtokenToBurn = new BigNumber(await shortOtoken.balanceOf(accountOwner1)) + const actionArgs = [ + { + actionType: ActionType.BurnShortOption, + owner: accountOwner1, + secondAddress: accountOperator1, + asset: shortOtoken.address, + vaultId: vaultCounter.toNumber(), + amount: shortOtokenToBurn.toString(), + index: '0', + data: ZERO_ADDR, + }, + ] + + await expectRevert( + controllerProxy.operate(actionArgs, { from: accountOperator1 }), + 'ERC20: burn amount exceeds balance', + ) + + // transfer back + await shortOtoken.transfer(accountOperator1, operatorShortBalance, { from: accountOwner1 }) + }) + + it('should revert burning when called from an address other than account owner or operator', async () => { + const vaultCounter = new BigNumber(await controllerProxy.getAccountVaultCounter(accountOwner1)) + assert.isAbove(vaultCounter.toNumber(), 0, 'Account owner have no vault') + + const shortOtokenToBurn = new BigNumber(await shortOtoken.balanceOf(accountOwner1)) + const actionArgs = [ + { + actionType: ActionType.BurnShortOption, + owner: accountOwner1, + secondAddress: accountOwner1, + asset: usdc.address, + vaultId: vaultCounter.toNumber(), + amount: shortOtokenToBurn.toString(), + index: '1', + data: ZERO_ADDR, + }, + ] + + await expectRevert(controllerProxy.operate(actionArgs, { from: random }), 'C6') + }) + + it('should revert minting short with invalid vault id', async () => { + const vaultCounter = new BigNumber('100') + + const shortOtokenToBurn = new BigNumber(await shortOtoken.balanceOf(accountOperator1)) + const actionArgs = [ + { + actionType: ActionType.BurnShortOption, + owner: accountOwner1, + secondAddress: accountOperator1, + asset: shortOtoken.address, + vaultId: vaultCounter.toNumber(), + amount: shortOtokenToBurn.toString(), + index: '0', + data: ZERO_ADDR, + }, + ] + + await expectRevert(controllerProxy.operate(actionArgs, { from: accountOwner1 }), 'C35') + }) + + it('should revert depositing long from an address that is not the msg.sender nor the owner account address', async () => { + const vaultCounter = new BigNumber(await controllerProxy.getAccountVaultCounter(accountOwner1)) + + const shortOtokenToBurn = new BigNumber(await shortOtoken.balanceOf(accountOperator1)) + const actionArgs = [ + { + actionType: ActionType.BurnShortOption, + owner: accountOwner1, + secondAddress: random, + asset: shortOtoken.address, + vaultId: vaultCounter.toNumber(), + amount: shortOtokenToBurn.toString(), + index: '0', + data: ZERO_ADDR, + }, + ] + + await expectRevert(controllerProxy.operate(actionArgs, { from: accountOperator1 }), 'C25') + }) + + it('should burn short otoken when called from account operator', async () => { + const vaultCounter = new BigNumber(await controllerProxy.getAccountVaultCounter(accountOwner1)) + assert.isAbove(vaultCounter.toNumber(), 0, 'Account owner have no vault') + + const vaultBefore = await controllerProxy.getVault(accountOwner1, vaultCounter) + + const shortOtokenToBurn = new BigNumber(await shortOtoken.balanceOf(accountOperator1)) + const actionArgs = [ + { + actionType: ActionType.BurnShortOption, + owner: accountOwner1, + secondAddress: accountOperator1, + asset: shortOtoken.address, + vaultId: vaultCounter.toNumber(), + amount: shortOtokenToBurn.toString(), + index: '0', + data: ZERO_ADDR, + }, + ] + const sellerBalanceBefore = new BigNumber(await shortOtoken.balanceOf(accountOperator1)) + + await controllerProxy.operate(actionArgs, { from: accountOperator1 }) + + const sellerBalanceAfter = new BigNumber(await shortOtoken.balanceOf(accountOperator1)) + const vaultAfter = await controllerProxy.getVault(accountOwner1, vaultCounter) + + assert.equal( + sellerBalanceBefore.minus(sellerBalanceAfter).toString(), + shortOtokenToBurn.toString(), + 'Short otoken burned amount mismatch', + ) + assert.equal(vaultAfter.shortOtokens.length, 1, 'Vault short otoken array length mismatch') + assert.equal( + vaultAfter.shortOtokens[0], + shortOtoken.address, + 'Vault short otoken address after burning mismatch', + ) + assert.equal( + new BigNumber(vaultBefore.shortAmounts[0]).minus(new BigNumber(vaultAfter.shortAmounts[0])).toString(), + shortOtokenToBurn.toString(), + 'Short otoken amount in vault after burn mismatch', + ) + }) + + it('should remove short otoken address from short otokens array if amount is equal to zero after burning', async () => { + // send back all short otoken to owner + const operatorShortBalance = new BigNumber(await shortOtoken.balanceOf(accountOperator1)) + await shortOtoken.transfer(accountOwner1, operatorShortBalance, { from: accountOperator1 }) + + const vaultCounter = new BigNumber(await controllerProxy.getAccountVaultCounter(accountOwner1)) + assert.isAbove(vaultCounter.toNumber(), 0, 'Account owner have no vault') + + const vaultBefore = await controllerProxy.getVault(accountOwner1, vaultCounter) + + const shortOtokenToBurn = new BigNumber(vaultBefore.shortAmounts[0]) + const actionArgs = [ + { + actionType: ActionType.BurnShortOption, + owner: accountOwner1, + secondAddress: accountOwner1, + asset: shortOtoken.address, + vaultId: vaultCounter.toNumber(), + amount: shortOtokenToBurn.toString(), + index: '0', + data: ZERO_ADDR, + }, + ] + const sellerBalanceBefore = new BigNumber(await shortOtoken.balanceOf(accountOwner1)) + + await controllerProxy.operate(actionArgs, { from: accountOwner1 }) + + const sellerBalanceAfter = new BigNumber(await shortOtoken.balanceOf(accountOwner1)) + const vaultAfter = await controllerProxy.getVault(accountOwner1, vaultCounter) + + assert.equal( + sellerBalanceBefore.minus(sellerBalanceAfter).toString(), + shortOtokenToBurn.toString(), + 'Short otoken burned amount mismatch', + ) + assert.equal(vaultAfter.shortOtokens.length, 1, 'Vault short otoken array length mismatch') + assert.equal(vaultAfter.shortOtokens[0], ZERO_ADDR, 'Vault short otoken address after clearing mismatch') + assert.equal( + new BigNumber(vaultBefore.shortAmounts[0]).minus(new BigNumber(vaultAfter.shortAmounts[0])).toString(), + shortOtokenToBurn.toString(), + 'Short otoken amount in vault after burn mismatch', + ) + }) + + it('should mint and burn at the same transaction', async () => { + const vaultCounter = new BigNumber(await controllerProxy.getAccountVaultCounter(accountOwner1)).plus(1) + // const vaultCounter = 1 + const amountToMint = createScaledNumber(1) + const actionArgs = [ + { + actionType: ActionType.OpenVault, + owner: accountOwner1, + secondAddress: accountOwner1, + asset: ZERO_ADDR, + vaultId: vaultCounter.toNumber(), + amount: '0', + index: '0', + data: ZERO_ADDR, + }, + { + actionType: ActionType.MintShortOption, + owner: accountOwner1, + secondAddress: accountOwner1, + asset: shortOtoken.address, + vaultId: vaultCounter.toNumber(), + amount: amountToMint, + index: '0', + data: ZERO_ADDR, + }, + { + actionType: ActionType.BurnShortOption, + owner: accountOwner1, + secondAddress: accountOwner1, + asset: shortOtoken.address, + vaultId: vaultCounter.toNumber(), + amount: amountToMint, + index: '0', + data: ZERO_ADDR, + }, + ] + const senderShortBalanceBefore = new BigNumber(await shortOtoken.balanceOf(accountOwner1)) + await controllerProxy.operate(actionArgs, { from: accountOwner1 }) + const senderShortBalanceAfter = new BigNumber(await shortOtoken.balanceOf(accountOwner1)) + const vaultAfter = await controllerProxy.getVault(accountOwner1, vaultCounter) + assert.equal(vaultAfter.shortOtokens.length, 1, 'Vault short otoken array length mismatch') + assert.equal(vaultAfter.shortOtokens[0], ZERO_ADDR) + assert.equal( + senderShortBalanceBefore.toString(), + senderShortBalanceAfter.toString(), + 'Sender short otoken amount mismatch', + ) + }) + + describe('Expired otoken', () => { + let expiredShortOtoken: MockOtokenInstance + + before(async () => { + const vaultCounterBefore = new BigNumber(await controllerProxy.getAccountVaultCounter(accountOwner1)) + + const expiryTime = new BigNumber(60 * 60) // after 1 hour + expiredShortOtoken = await MockOtoken.new() + // init otoken + await expiredShortOtoken.init( + addressBook.address, + weth.address, + usdc.address, + usdc.address, + createTokenAmount(200), + new BigNumber(await time.latest()).plus(expiryTime), + true, + ) + + // whitelist otoken to be minted + await whitelist.whitelistOtoken(expiredShortOtoken.address, { from: owner }) + + const collateralToDeposit = new BigNumber(await expiredShortOtoken.strikePrice()).dividedBy(100) + const amountToMint = createTokenAmount(1) + const actionArgs = [ + { + actionType: ActionType.OpenVault, + owner: accountOwner1, + secondAddress: accountOwner1, + asset: ZERO_ADDR, + vaultId: vaultCounterBefore.toNumber() + 1, + amount: '0', + index: '0', + data: ZERO_ADDR, + }, + { + actionType: ActionType.MintShortOption, + owner: accountOwner1, + secondAddress: accountOwner1, + asset: expiredShortOtoken.address, + vaultId: vaultCounterBefore.toNumber() + 1, + amount: amountToMint, + index: '0', + data: ZERO_ADDR, + }, + { + actionType: ActionType.DepositCollateral, + owner: accountOwner1, + secondAddress: accountOwner1, + asset: usdc.address, + vaultId: vaultCounterBefore.toNumber() + 1, + amount: collateralToDeposit.toNumber(), + index: '0', + data: ZERO_ADDR, + }, + ] + + const finalMarginPool = (await borrowableMarginPool.isWhitelistedOptionsVault(accountOwner1)) + ? borrowableMarginPool.address + : marginPool.address + + const marginPoolBalanceBefore = new BigNumber(await usdc.balanceOf(finalMarginPool)) + const senderBalanceBefore = new BigNumber(await usdc.balanceOf(accountOwner1)) + + await usdc.approve(finalMarginPool, collateralToDeposit, { from: accountOwner1 }) + await controllerProxy.operate(actionArgs, { from: accountOwner1 }) + + const marginPoolBalanceAfter = new BigNumber(await usdc.balanceOf(finalMarginPool)) + const senderBalanceAfter = new BigNumber(await usdc.balanceOf(accountOwner1)) + + assert.equal( + marginPoolBalanceAfter.minus(marginPoolBalanceBefore).toString(), + collateralToDeposit.toString(), + 'Margin pool collateral asset balance mismatch', + ) + assert.equal( + senderBalanceBefore.minus(senderBalanceAfter).toString(), + collateralToDeposit.toString(), + 'Sender collateral asset balance mismatch', + ) + }) + + it('should revert burning an expired short otoken', async () => { + // increment time after expiredLongOtoken expiry + await time.increase(3601) // increase time with one hour in seconds + + const vaultId = new BigNumber(await controllerProxy.getAccountVaultCounter(accountOwner1)) + const shortAmountToBurn = new BigNumber('1') + const actionArgs = [ + { + actionType: ActionType.BurnShortOption, + owner: accountOwner1, + secondAddress: accountOwner1, + asset: expiredShortOtoken.address, + vaultId: vaultId.toNumber(), + amount: shortAmountToBurn.toNumber(), + index: '0', + data: ZERO_ADDR, + }, + ] + + assert.equal( + await controllerProxy.hasExpired(expiredShortOtoken.address), + true, + 'Long otoken is not expired yet', + ) + + await expectRevert(controllerProxy.operate(actionArgs, { from: accountOwner1 }), 'C26') + }) + + it('should revert minting an expired short otoken', async () => { + const vaultCounter = new BigNumber(await controllerProxy.getAccountVaultCounter(accountOwner1)) + const finalMarginPool = (await borrowableMarginPool.isWhitelistedOptionsVault(accountOwner1)) + ? borrowableMarginPool.address + : marginPool.address + assert.isAbove(vaultCounter.toNumber(), 0, 'Account owner have no vault') + + const collateralToDeposit = new BigNumber(await expiredShortOtoken.strikePrice()).dividedBy(100) + const amountToMint = createTokenAmount(1) + const actionArgs = [ + { + actionType: ActionType.MintShortOption, + owner: accountOwner1, + secondAddress: accountOwner1, + asset: expiredShortOtoken.address, + vaultId: vaultCounter.toNumber(), + amount: amountToMint, + index: '0', + data: ZERO_ADDR, + }, + { + actionType: ActionType.DepositCollateral, + owner: accountOwner1, + secondAddress: accountOwner1, + asset: usdc.address, + vaultId: vaultCounter.toNumber(), + amount: collateralToDeposit.toNumber(), + index: '0', + data: ZERO_ADDR, + }, + ] + + await usdc.approve(finalMarginPool, collateralToDeposit, { from: accountOperator1 }) + await expectRevert(controllerProxy.operate(actionArgs, { from: accountOwner1 }), 'C24') + }) + + it('should revert withdraw collateral from a vault with an expired short otoken', async () => { + const vaultCounter = new BigNumber(await controllerProxy.getAccountVaultCounter(accountOwner1)) + assert.isAbove(vaultCounter.toNumber(), 0, 'Account owner have no vault') + + const collateralToWithdraw = createTokenAmount(10, usdcDecimals) + const actionArgs = [ + { + actionType: ActionType.WithdrawCollateral, + owner: accountOwner1, + secondAddress: accountOwner1, + asset: usdc.address, + vaultId: vaultCounter.toNumber(), + amount: collateralToWithdraw, + index: '0', + data: ZERO_ADDR, + }, + ] + + await expectRevert(controllerProxy.operate(actionArgs, { from: accountOwner1 }), 'C22') + }) + }) + }) + }) + + describe('Redeem', () => { + let shortOtoken: MockOtokenInstance + let fakeOtoken: MockOtokenInstance + let finalMarginPool: string + + before(async () => { + const expiryTime = new BigNumber(60 * 60 * 24) // after 1 day + + shortOtoken = await MockOtoken.new() + // init otoken + await shortOtoken.init( + addressBook.address, + weth.address, + usdc.address, + usdc.address, + createTokenAmount(200), + new BigNumber(await time.latest()).plus(expiryTime), + true, + ) + + fakeOtoken = await MockOtoken.new() + // init otoken + await fakeOtoken.init( + addressBook.address, + weth.address, + usdc.address, + usdc.address, + createTokenAmount(200), + new BigNumber(await time.latest()).plus(expiryTime), + true, + ) + + // whitelist short otoken to be used in the protocol + await whitelist.whitelistOtoken(shortOtoken.address, { from: owner }) + // open new vault, mintnaked short, sell it to holder 1 + const vaultCounter = new BigNumber(await controllerProxy.getAccountVaultCounter(accountOwner1)).plus(1) + const collateralToDeposit = new BigNumber(await shortOtoken.strikePrice()).dividedBy(100) + const amountToMint = createTokenAmount(1) + const actionArgs = [ + { + actionType: ActionType.OpenVault, + owner: accountOwner1, + secondAddress: accountOwner1, + asset: ZERO_ADDR, + vaultId: vaultCounter.toNumber(), + amount: '0', + index: '0', + data: ZERO_ADDR, + }, + { + actionType: ActionType.MintShortOption, + owner: accountOwner1, + secondAddress: accountOwner1, + asset: shortOtoken.address, + vaultId: vaultCounter.toNumber(), + amount: amountToMint, + index: '0', + data: ZERO_ADDR, + }, + { + actionType: ActionType.DepositCollateral, + owner: accountOwner1, + secondAddress: accountOwner1, + asset: usdc.address, + vaultId: vaultCounter.toNumber(), + amount: collateralToDeposit.toNumber(), + index: '0', + data: ZERO_ADDR, + }, + ] + await borrowableMarginPool.setOTokenBuyerWhitelistedStatus(holder1, true, { from: owner }) + finalMarginPool = + !(await borrowableMarginPool.isWhitelistedOTokenBuyer(holder1)) || + new BigNumber(await usdc.balanceOf(borrowableMarginPool.address)).isLessThan(collateralToDeposit) + ? marginPool.address + : borrowableMarginPool.address + + await usdc.approve(finalMarginPool, collateralToDeposit, { from: accountOwner1 }) + await controllerProxy.operate(actionArgs, { from: accountOwner1 }) + // transfer minted short otoken to hodler` + await shortOtoken.transfer(holder1, amountToMint.toString(), { from: accountOwner1 }) + }) + + it('should revert exercising non-whitelisted otoken', async () => { + const shortAmountToBurn = new BigNumber('1') + const actionArgs = [ + { + actionType: ActionType.Redeem, + owner: ZERO_ADDR, + secondAddress: holder1, + asset: fakeOtoken.address, + vaultId: '0', + amount: shortAmountToBurn.toNumber(), + index: '0', + data: ZERO_ADDR, + }, + ] + + await expectRevert(controllerProxy.operate(actionArgs, { from: holder1 }), 'C27') + }) + + it('should revert exercising un-expired otoken', async () => { + const shortAmountToBurn = new BigNumber('1') + const actionArgs = [ + { + actionType: ActionType.Redeem, + owner: ZERO_ADDR, + secondAddress: holder1, + asset: shortOtoken.address, + vaultId: '0', + amount: shortAmountToBurn.toNumber(), + index: '0', + data: ZERO_ADDR, + }, + ] + + assert.equal(await controllerProxy.hasExpired(shortOtoken.address), false, 'Short otoken has already expired') + + await expectRevert(controllerProxy.operate(actionArgs, { from: holder1 }), 'C28') + }) + + it('should revert exercising after expiry, when underlying price is not finalized yet', async () => { + // past time after expiry + await time.increase(60 * 61 * 24) // increase time with one hour in seconds + // set price in Oracle Mock, 150$ at expiry, expire ITM + await oracle.setExpiryPriceFinalizedAllPeiodOver( + await shortOtoken.underlyingAsset(), + new BigNumber(await shortOtoken.expiryTimestamp()), + createTokenAmount(150), + true, + ) + // set it as not finalized in mock + await oracle.setIsDisputePeriodOver( + await shortOtoken.underlyingAsset(), + new BigNumber(await shortOtoken.expiryTimestamp()), + false, + ) + + await oracle.setExpiryPriceFinalizedAllPeiodOver( + await shortOtoken.strikeAsset(), + new BigNumber(await shortOtoken.expiryTimestamp()), + createTokenAmount(1), + true, + ) + + const shortAmountToBurn = new BigNumber('1') + const actionArgs = [ + { + actionType: ActionType.Redeem, + owner: ZERO_ADDR, + secondAddress: holder1, + asset: shortOtoken.address, + vaultId: '0', + amount: shortAmountToBurn.toNumber(), + index: '0', + data: ZERO_ADDR, + }, + ] + + assert.equal(await controllerProxy.hasExpired(shortOtoken.address), true, 'Short otoken is not expired yet') + + await expectRevert(controllerProxy.operate(actionArgs, { from: holder1 }), 'C29') + }) + + it('should revert exercising if cash value receiver address in equal to address zero', async () => { + // set it as finalized in mock + await oracle.setIsDisputePeriodOver( + await shortOtoken.underlyingAsset(), + new BigNumber(await shortOtoken.expiryTimestamp()), + true, + ) + + const shortAmountToBurn = createTokenAmount(1) + const actionArgs = [ + { + actionType: ActionType.Redeem, + owner: ZERO_ADDR, + secondAddress: ZERO_ADDR, + asset: shortOtoken.address, + vaultId: '0', + amount: shortAmountToBurn, + index: '0', + data: ZERO_ADDR, + }, + ] + + assert.equal(await controllerProxy.hasExpired(shortOtoken.address), true, 'Short otoken is not expired yet') + + await expectRevert(controllerProxy.operate(actionArgs, { from: holder1 }), 'A14') + }) + + it('should redeem after expiry + price is finalized', async () => { + const shortAmountToBurn = createTokenAmount(1) + const actionArgs = [ + { + actionType: ActionType.Redeem, + owner: ZERO_ADDR, + secondAddress: holder1, + asset: shortOtoken.address, + vaultId: '0', + amount: shortAmountToBurn, + index: '0', + data: ZERO_ADDR, + }, + ] + assert.equal(await controllerProxy.hasExpired(shortOtoken.address), true, 'Short otoken is not expired yet') + + const payout = createTokenAmount(50, usdcDecimals) + const marginPoolBalanceBefore = new BigNumber(await usdc.balanceOf(finalMarginPool)) + const senderBalanceBefore = new BigNumber(await usdc.balanceOf(holder1)) + const senderShortBalanceBefore = new BigNumber(await shortOtoken.balanceOf(holder1)) + + await controllerProxy.operate(actionArgs, { from: holder1 }) + + const marginPoolBalanceAfter = new BigNumber(await usdc.balanceOf(finalMarginPool)) + const senderBalanceAfter = new BigNumber(await usdc.balanceOf(holder1)) + const senderShortBalanceAfter = new BigNumber(await shortOtoken.balanceOf(holder1)) + + assert.equal( + marginPoolBalanceBefore.minus(marginPoolBalanceAfter).toString(), + payout.toString(), + 'Margin pool collateral asset balance mismatch', + ) + assert.equal( + senderBalanceAfter.minus(senderBalanceBefore).toString(), + payout.toString(), + 'Sender collateral asset balance mismatch', + ) + assert.equal( + senderShortBalanceBefore.minus(senderShortBalanceAfter).toString(), + shortAmountToBurn.toString(), + ' Burned short otoken amount mismatch', + ) + }) + + it('should redeem call option correctly', async () => { + const expiry = new BigNumber(await time.latest()).plus(new BigNumber(60 * 60)).toNumber() + const call: MockOtokenInstance = await MockOtoken.new() + await call.init( + addressBook.address, + weth.address, + usdc.address, + weth.address, + createTokenAmount(200), + expiry, + false, + ) + + await whitelist.whitelistOtoken(call.address) + const vaultCounter = new BigNumber(await controllerProxy.getAccountVaultCounter(accountOwner1)).plus(1) + const amountCollateral = createTokenAmount(1, wethDecimals) + await weth.mint(accountOwner1, amountCollateral) + await weth.approve(finalMarginPool, amountCollateral, { from: accountOwner1 }) + const amountOtoken = createTokenAmount(1) + const actionArgs = [ + { + actionType: ActionType.OpenVault, + owner: accountOwner1, + secondAddress: accountOwner1, + asset: ZERO_ADDR, + vaultId: vaultCounter.toNumber(), + amount: '0', + index: '0', + data: ZERO_ADDR, + }, + { + actionType: ActionType.DepositCollateral, + owner: accountOwner1, + secondAddress: accountOwner1, + asset: weth.address, + vaultId: vaultCounter.toNumber(), + amount: amountCollateral, + index: '0', + data: ZERO_ADDR, + }, + { + actionType: ActionType.MintShortOption, + owner: accountOwner1, + secondAddress: accountOwner1, + asset: call.address, + vaultId: vaultCounter.toNumber(), + amount: amountOtoken, + index: '0', + data: ZERO_ADDR, + }, + ] + + await controllerProxy.operate(actionArgs, { from: accountOwner1 }) + await call.transfer(holder1, amountOtoken, { from: accountOwner1 }) + + await time.increaseTo(expiry + 10) + await oracle.setExpiryPriceFinalizedAllPeiodOver(weth.address, expiry, createTokenAmount(400), true) + await oracle.setExpiryPriceFinalizedAllPeiodOver(usdc.address, expiry, createTokenAmount(1), true) + const redeemArgs = [ + { + actionType: ActionType.Redeem, + owner: ZERO_ADDR, + secondAddress: holder1, + asset: call.address, + vaultId: '0', + amount: amountOtoken, + index: '0', + data: ZERO_ADDR, + }, + ] + + const expectedPayout = createTokenAmount(0.5, wethDecimals) + + const userBalanceBefore = new BigNumber(await weth.balanceOf(holder1)) + await controllerProxy.operate(redeemArgs, { from: holder1 }) + const userBalanceAfter = new BigNumber(await weth.balanceOf(holder1)) + assert.equal(userBalanceAfter.minus(userBalanceBefore).toString(), expectedPayout) + }) + + it('should revert redeem option if collateral is different from underlying, and collateral price is not finalized', async () => { + const expiry = new BigNumber(await time.latest()).plus(new BigNumber(60 * 60)).toNumber() + + await whitelist.whitelistCollateral(weth2.address) + const call: MockOtokenInstance = await MockOtoken.new() + await call.init( + addressBook.address, + weth.address, + usdc.address, + weth2.address, + createTokenAmount(200), + expiry, + false, + ) + + await oracle.setRealTimePrice(weth.address, createTokenAmount(400)) + await oracle.setRealTimePrice(weth2.address, createTokenAmount(400)) + + await whitelist.whitelistOtoken(call.address) + const vaultCounter = new BigNumber(await controllerProxy.getAccountVaultCounter(accountOwner1)).plus(1) + const amountCollateral = createTokenAmount(1, wethDecimals) + await weth2.mint(accountOwner1, amountCollateral) + await weth2.approve(finalMarginPool, amountCollateral, { from: accountOwner1 }) + + const amountOtoken = createTokenAmount(1) + const actionArgs = [ + { + actionType: ActionType.OpenVault, + owner: accountOwner1, + secondAddress: accountOwner1, + asset: ZERO_ADDR, + vaultId: vaultCounter.toNumber(), + amount: '0', + index: '0', + data: ZERO_ADDR, + }, + { + actionType: ActionType.MintShortOption, + owner: accountOwner1, + secondAddress: accountOwner1, + asset: call.address, + vaultId: vaultCounter.toNumber(), + amount: amountOtoken, + index: '0', + data: ZERO_ADDR, + }, + { + actionType: ActionType.DepositCollateral, + owner: accountOwner1, + secondAddress: accountOwner1, + asset: weth2.address, + vaultId: vaultCounter.toNumber(), + amount: amountCollateral, + index: '0', + data: ZERO_ADDR, + }, + ] + + await controllerProxy.operate(actionArgs, { from: accountOwner1 }) + await call.transfer(holder1, amountOtoken, { from: accountOwner1 }) + + await time.increaseTo(expiry + 10) + await oracle.setExpiryPriceFinalizedAllPeiodOver(weth.address, expiry, createTokenAmount(400), true) + await oracle.setExpiryPriceFinalizedAllPeiodOver(usdc.address, expiry, createTokenAmount(1), true) + + const redeemArgs = [ + { + actionType: ActionType.Redeem, + owner: ZERO_ADDR, + secondAddress: holder1, + asset: call.address, + vaultId: '0', + amount: amountOtoken, + index: '0', + data: ZERO_ADDR, + }, + ] + + await expectRevert(controllerProxy.operate(redeemArgs, { from: holder1 }), 'C29') + }) + + describe('Redeem multiple Otokens', () => { + let firstOtoken: MockOtokenInstance + let secondOtoken: MockOtokenInstance + + before(async () => { + const expiryTime = new BigNumber(60 * 60 * 24) // after 1 day + + firstOtoken = await MockOtoken.new() + secondOtoken = await MockOtoken.new() + + const expiry = new BigNumber(await time.latest()).plus(expiryTime) + // init otoken + await firstOtoken.init( + addressBook.address, + weth.address, + usdc.address, + usdc.address, + createTokenAmount(200), + expiry, + true, + ) + await secondOtoken.init( + addressBook.address, + weth.address, + usdc.address, + usdc.address, + createTokenAmount(200), + expiry, + true, + ) + // whitelist otoken to be used in the protocol + await whitelist.whitelistOtoken(firstOtoken.address, { from: owner }) + await whitelist.whitelistOtoken(secondOtoken.address, { from: owner }) + + // open new vault, mint naked short, sell it to holder 1 + const firstCollateralToDeposit = new BigNumber(await firstOtoken.strikePrice()).dividedBy(100) + const secondCollateralToDeposit = new BigNumber(await secondOtoken.strikePrice()).dividedBy(100) + const amountToMint = createTokenAmount(1) + let vaultCounter = new BigNumber(await controllerProxy.getAccountVaultCounter(accountOwner1)).plus(1) + let actionArgs = [ + { + actionType: ActionType.OpenVault, + owner: accountOwner1, + secondAddress: accountOwner1, + asset: ZERO_ADDR, + vaultId: vaultCounter.toNumber(), + amount: '0', + index: '0', + data: ZERO_ADDR, + }, + { + actionType: ActionType.MintShortOption, + owner: accountOwner1, + secondAddress: accountOwner1, + asset: firstOtoken.address, + vaultId: vaultCounter.toNumber(), + amount: amountToMint, + index: '0', + data: ZERO_ADDR, + }, + { + actionType: ActionType.DepositCollateral, + owner: accountOwner1, + secondAddress: accountOwner1, + asset: usdc.address, + vaultId: vaultCounter.toNumber(), + amount: firstCollateralToDeposit.toNumber(), + index: '0', + data: ZERO_ADDR, + }, + ] + await usdc.approve(finalMarginPool, firstCollateralToDeposit, { from: accountOwner1 }) + await controllerProxy.operate(actionArgs, { from: accountOwner1 }) + + vaultCounter = new BigNumber(await controllerProxy.getAccountVaultCounter(accountOwner1)).plus(1) + actionArgs = [ + { + actionType: ActionType.OpenVault, + owner: accountOwner1, + secondAddress: accountOwner1, + asset: ZERO_ADDR, + vaultId: vaultCounter.toNumber(), + amount: '0', + index: '0', + data: ZERO_ADDR, + }, + { + actionType: ActionType.MintShortOption, + owner: accountOwner1, + secondAddress: accountOwner1, + asset: secondOtoken.address, + vaultId: vaultCounter.toNumber(), + amount: amountToMint, + index: '0', + data: ZERO_ADDR, + }, + { + actionType: ActionType.DepositCollateral, + owner: accountOwner1, + secondAddress: accountOwner1, + asset: usdc.address, + vaultId: vaultCounter.toNumber(), + amount: secondCollateralToDeposit.toNumber(), + index: '0', + data: ZERO_ADDR, + }, + ] + await usdc.approve(finalMarginPool, firstCollateralToDeposit, { from: accountOwner1 }) + await controllerProxy.operate(actionArgs, { from: accountOwner1 }) + // transfer minted short otoken to hodler + await firstOtoken.transfer(holder1, amountToMint, { from: accountOwner1 }) + await secondOtoken.transfer(holder1, amountToMint, { from: accountOwner1 }) + }) + + it('should redeem multiple Otokens in one transaction', async () => { + // past time after expiry + await time.increase(60 * 61 * 24) + // set price in Oracle Mock, 150$ at expiry, expire ITM + await oracle.setExpiryPriceFinalizedAllPeiodOver( + await firstOtoken.underlyingAsset(), + new BigNumber(await firstOtoken.expiryTimestamp()), + createTokenAmount(150), + true, + ) + await oracle.setExpiryPriceFinalizedAllPeiodOver( + await secondOtoken.underlyingAsset(), + new BigNumber(await secondOtoken.expiryTimestamp()), + createTokenAmount(150), + true, + ) + + await oracle.setExpiryPriceFinalizedAllPeiodOver( + await firstOtoken.strikeAsset(), + new BigNumber(await firstOtoken.expiryTimestamp()), + createTokenAmount(1), + true, + ) + await oracle.setExpiryPriceFinalizedAllPeiodOver( + await secondOtoken.strikeAsset(), + new BigNumber(await firstOtoken.expiryTimestamp()), + createTokenAmount(1), + true, + ) + + const amountToRedeem = createTokenAmount(1) + const actionArgs = [ + { + actionType: ActionType.Redeem, + owner: ZERO_ADDR, + secondAddress: holder1, + asset: firstOtoken.address, + vaultId: '0', + amount: amountToRedeem, + index: '0', + data: ZERO_ADDR, + }, + { + actionType: ActionType.Redeem, + owner: ZERO_ADDR, + secondAddress: holder1, + asset: secondOtoken.address, + vaultId: '0', + amount: amountToRedeem, + index: '0', + data: ZERO_ADDR, + }, + ] + + const payout = createTokenAmount(100, usdcDecimals) + const marginPoolBalanceBefore = new BigNumber(await usdc.balanceOf(finalMarginPool)) + const senderBalanceBefore = new BigNumber(await usdc.balanceOf(holder1)) + + await controllerProxy.operate(actionArgs, { from: holder1 }) + + const marginPoolBalanceAfter = new BigNumber(await usdc.balanceOf(finalMarginPool)) + const senderBalanceAfter = new BigNumber(await usdc.balanceOf(holder1)) + const senderFirstBalanceAfter = new BigNumber(await firstOtoken.balanceOf(holder1)) + const senderSecondBalanceAfter = new BigNumber(await secondOtoken.balanceOf(holder1)) + + assert.equal( + marginPoolBalanceBefore.minus(marginPoolBalanceAfter).toString(), + payout.toString(), + 'Margin pool collateral asset balance mismatch', + ) + assert.equal( + senderBalanceAfter.minus(senderBalanceBefore).toString(), + payout.toString(), + 'Sender collateral asset balance mismatch', + ) + assert.equal(senderFirstBalanceAfter.toString(), '0', ' Burned first otoken amount mismatch') + assert.equal(senderSecondBalanceAfter.toString(), '0', ' Burned first otoken amount mismatch') + }) + }) + }) + + describe('Settle vault', () => { + let shortOtoken: MockOtokenInstance + + before(async () => { + const expiryTime = new BigNumber(60 * 60 * 24) // after 1 day + + shortOtoken = await MockOtoken.new() + // init otoken + await shortOtoken.init( + addressBook.address, + weth.address, + usdc.address, + usdc.address, + createTokenAmount(200), + new BigNumber(await time.latest()).plus(expiryTime), + true, + ) + // whitelist otoken to be used in the protocol + await whitelist.whitelistOtoken(shortOtoken.address, { from: owner }) + // open new vault, mint naked short, sell it to holder 1 + const collateralToDespoit = new BigNumber(await shortOtoken.strikePrice()).dividedBy(100) + const vaultCounter = new BigNumber(await controllerProxy.getAccountVaultCounter(accountOwner1)).plus(1) + + const actionArgs = [ + { + actionType: ActionType.OpenVault, + owner: accountOwner1, + secondAddress: accountOwner1, + asset: ZERO_ADDR, + vaultId: vaultCounter.toNumber(), + amount: '0', + index: '0', + data: ZERO_ADDR, + }, + { + actionType: ActionType.DepositCollateral, + owner: accountOwner1, + secondAddress: accountOwner1, + asset: usdc.address, + vaultId: vaultCounter.toNumber(), + amount: collateralToDespoit.toNumber(), + index: '0', + data: ZERO_ADDR, + }, + ] + const finalMarginPool = (await borrowableMarginPool.isWhitelistedOptionsVault(accountOwner1)) + ? borrowableMarginPool.address + : marginPool.address + await usdc.approve(finalMarginPool, collateralToDespoit, { from: accountOwner1 }) + await controllerProxy.operate(actionArgs, { from: accountOwner1 }) + }) + + it('should revert settling a vault that have no long or short otoken', async () => { + const vaultCounter = new BigNumber(await controllerProxy.getAccountVaultCounter(accountOwner1)) + const actionArgs = [ + { + actionType: ActionType.SettleVault, + owner: accountOwner1, + secondAddress: accountOwner1, + asset: shortOtoken.address, + vaultId: vaultCounter.toNumber(), + amount: '0', + index: '0', + data: ZERO_ADDR, + }, + ] + + await expectRevert(controllerProxy.operate(actionArgs, { from: accountOwner1 }), 'C30') + }) + + it('should revert settling vault before expiry', async () => { + // mint token in vault before + const amountToMint = createTokenAmount(1) + let vaultCounter = new BigNumber(await controllerProxy.getAccountVaultCounter(accountOwner1)) + + let actionArgs = [ + { + actionType: ActionType.MintShortOption, + owner: accountOwner1, + secondAddress: accountOwner1, + asset: shortOtoken.address, + vaultId: vaultCounter.toNumber(), + amount: amountToMint, + index: '0', + data: ZERO_ADDR, + }, + ] + await controllerProxy.operate(actionArgs, { from: accountOwner1 }) + await shortOtoken.transfer(holder1, amountToMint, { from: accountOwner1 }) + + vaultCounter = new BigNumber(await controllerProxy.getAccountVaultCounter(accountOwner1)) + actionArgs = [ + { + actionType: ActionType.SettleVault, + owner: accountOwner1, + secondAddress: accountOwner1, + asset: shortOtoken.address, + vaultId: vaultCounter.toNumber(), + amount: '0', + index: '0', + data: ZERO_ADDR, + }, + ] + + await expectRevert(controllerProxy.operate(actionArgs, { from: accountOwner1 }), 'C31') + }) + + it('should revert settling an invalid vault', async () => { + const vaultCounter = new BigNumber(await controllerProxy.getAccountVaultCounter(accountOwner1)) + const actionArgs = [ + { + actionType: ActionType.SettleVault, + owner: accountOwner1, + secondAddress: accountOwner1, + asset: shortOtoken.address, + vaultId: vaultCounter.plus(10000).toNumber(), + amount: '0', + index: '0', + data: ZERO_ADDR, + }, + ] + + await expectRevert(controllerProxy.operate(actionArgs, { from: accountOwner1 }), 'C35') + }) + + it('should revert settling after expiry when price is not finalized', async () => { + // past time after expiry + await time.increase(60 * 61 * 24) // increase time with one hour in seconds + // set price in Oracle Mock, 150$ at expiry, expire ITM + const expiry = new BigNumber(await shortOtoken.expiryTimestamp()) + await oracle.setExpiryPriceFinalizedAllPeiodOver(weth.address, expiry, createTokenAmount(150), true) + // set it as not finalized in mock + await oracle.setIsFinalized( + await shortOtoken.underlyingAsset(), + new BigNumber(await shortOtoken.expiryTimestamp()), + false, + ) + await oracle.setIsDisputePeriodOver( + await shortOtoken.underlyingAsset(), + new BigNumber(await shortOtoken.expiryTimestamp()), + false, + ) + + const vaultCounter = new BigNumber(await controllerProxy.getAccountVaultCounter(accountOwner1)) + const actionArgs = [ + { + actionType: ActionType.SettleVault, + owner: accountOwner1, + secondAddress: accountOwner1, + asset: shortOtoken.address, + vaultId: vaultCounter.toNumber(), + amount: '0', + index: '0', + data: ZERO_ADDR, + }, + ] + + assert.equal(await controllerProxy.hasExpired(shortOtoken.address), true, 'Short otoken is not expired yet') + + await expectRevert(controllerProxy.operate(actionArgs, { from: accountOwner1 }), 'C29') + }) + + it('should settle ITM otoken after expiry + price is finalized', async () => { + const expiry = new BigNumber(await shortOtoken.expiryTimestamp()) + await oracle.setExpiryPriceFinalizedAllPeiodOver(weth.address, expiry, createTokenAmount(150), true) + await oracle.setExpiryPriceFinalizedAllPeiodOver(usdc.address, expiry, createTokenAmount(1), true) + const vaultCounter = new BigNumber(await controllerProxy.getAccountVaultCounter(accountOwner1)) + const finalMarginPool = (await borrowableMarginPool.isWhitelistedOptionsVault(accountOwner1)) + ? borrowableMarginPool.address + : marginPool.address + + const actionArgs = [ + { + actionType: ActionType.SettleVault, + owner: accountOwner1, + secondAddress: accountOwner1, + asset: shortOtoken.address, + vaultId: vaultCounter.toNumber(), + amount: '0', + index: '0', + data: ZERO_ADDR, + }, + ] + + const payout = createTokenAmount(150, usdcDecimals) + const marginPoolBalanceBefore = new BigNumber(await usdc.balanceOf(finalMarginPool)) + const senderBalanceBefore = new BigNumber(await usdc.balanceOf(accountOwner1)) + const proceed = await controllerProxy.getProceed(accountOwner1, vaultCounter) + + assert.equal(payout, proceed.toString()) + + await controllerProxy.operate(actionArgs, { from: accountOwner1 }) + + const marginPoolBalanceAfter = new BigNumber(await usdc.balanceOf(finalMarginPool)) + const senderBalanceAfter = new BigNumber(await usdc.balanceOf(accountOwner1)) + + assert.equal( + marginPoolBalanceBefore.minus(marginPoolBalanceAfter).toString(), + payout.toString(), + 'Margin pool collateral asset balance mismatch', + ) + assert.equal( + senderBalanceAfter.minus(senderBalanceBefore).toString(), + payout.toString(), + 'Sender collateral asset balance mismatch', + ) + }) + + it('should settle vault with only long otokens in it', async () => { + const stirkePrice = 250 + const expiry = new BigNumber(await time.latest()).plus(86400) + const longOtoken: MockOtokenInstance = await MockOtoken.new() + // create a new otoken + await longOtoken.init( + addressBook.address, + weth.address, + usdc.address, + usdc.address, + createScaledNumber(stirkePrice), + expiry, + true, + ) + + await whitelist.whitelistOtoken(longOtoken.address) + + // mint some long otokens, (so we can put it as long) + const vaultCounter = new BigNumber(await controllerProxy.getAccountVaultCounter(accountOwner1)).plus(1) + const finalMarginPool = (await borrowableMarginPool.isWhitelistedOptionsVault(accountOwner1)) + ? borrowableMarginPool.address + : marginPool.address + + const longAmount = createTokenAmount(1) + const collateralAmount = createTokenAmount(stirkePrice, usdcDecimals) + const mintArgs = [ + { + actionType: ActionType.OpenVault, + owner: accountOwner1, + secondAddress: accountOwner1, + asset: ZERO_ADDR, + vaultId: vaultCounter.toNumber(), + amount: '0', + index: '0', + data: ZERO_ADDR, + }, + { + actionType: ActionType.MintShortOption, + owner: accountOwner1, + secondAddress: accountOwner1, + asset: longOtoken.address, + vaultId: vaultCounter.toNumber(), + amount: longAmount, + index: '0', + data: ZERO_ADDR, + }, + { + actionType: ActionType.DepositCollateral, + owner: accountOwner1, + secondAddress: accountOwner1, + asset: usdc.address, + vaultId: vaultCounter.toNumber(), + amount: collateralAmount, + index: '0', + data: ZERO_ADDR, + }, + ] + await usdc.approve(finalMarginPool, collateralAmount, { from: accountOwner1 }) + await controllerProxy.operate(mintArgs, { from: accountOwner1 }) + // Use the newly minted otoken as long and put it in a new vault + const newVaultId = vaultCounter.toNumber() + 1 + + const newVaultArgs = [ + { + actionType: ActionType.OpenVault, + owner: accountOwner1, + secondAddress: accountOwner1, + asset: ZERO_ADDR, + vaultId: newVaultId, + amount: '0', + index: '0', + data: ZERO_ADDR, + }, + { + actionType: ActionType.DepositLongOption, + owner: accountOwner1, + secondAddress: accountOwner1, + asset: longOtoken.address, + vaultId: newVaultId, + amount: longAmount, + index: '0', + data: ZERO_ADDR, + }, + ] + await longOtoken.approve(finalMarginPool, longAmount, { from: accountOwner1 }) + await whitelist.whitelistOtoken(longOtoken.address) + await controllerProxy.operate(newVaultArgs, { from: accountOwner1 }) + // go to expiry + await time.increaseTo(expiry.toNumber() + 10) + const ethPriceAtExpiry = 200 + await oracle.setExpiryPriceFinalizedAllPeiodOver(usdc.address, expiry, createScaledNumber(1), true) + await oracle.setExpiryPriceFinalizedAllPeiodOver( + weth.address, + expiry, + createScaledNumber(ethPriceAtExpiry), + true, + ) + // settle the secont vault (with only long otoken in it) + const settleArgs = [ + { + actionType: ActionType.SettleVault, + owner: accountOwner1, + secondAddress: accountOwner1, + asset: ZERO_ADDR, + vaultId: newVaultId, + amount: '0', + index: '0', + data: ZERO_ADDR, + }, + ] + const expectedPayout = new BigNumber(createTokenAmount(stirkePrice - ethPriceAtExpiry, usdcDecimals)) + const ownerUSDCBalanceBefore = new BigNumber(await usdc.balanceOf(accountOwner1)) + const poolOtokenBefore = new BigNumber(await longOtoken.balanceOf(finalMarginPool)) + + const amountPayout = await controllerProxy.getProceed(accountOwner1, newVaultId) + + assert.equal(amountPayout.toString(), expectedPayout.toString(), 'payout calculation mismatch') + await controllerProxy.operate(settleArgs, { from: accountOwner1 }) + const ownerUSDCBalanceAfter = new BigNumber(await usdc.balanceOf(accountOwner1)) + const poolOtokenAfter = new BigNumber(await longOtoken.balanceOf(finalMarginPool)) + assert.equal( + ownerUSDCBalanceAfter.toString(), + ownerUSDCBalanceBefore.plus(amountPayout).toString(), + 'settle long vault payout mismatch', + ) + assert.equal( + poolOtokenAfter.toString(), + poolOtokenBefore.minus(new BigNumber(longAmount)).toString(), + 'settle long vault otoken mismatch', + ) + }) + + describe('Settle multiple vaults ATM and OTM', () => { + let firstShortOtoken: MockOtokenInstance + let secondShortOtoken: MockOtokenInstance + + before(async () => { + let expiryTime = new BigNumber(60 * 60 * 24) // after 1 day + + firstShortOtoken = await MockOtoken.new() + await firstShortOtoken.init( + addressBook.address, + weth.address, + usdc.address, + usdc.address, + createTokenAmount(200), + new BigNumber(await time.latest()).plus(expiryTime), + true, + ) + // whitelist otoken to be used in the protocol + await whitelist.whitelistOtoken(firstShortOtoken.address, { from: owner }) + // open new vault, mint naked short, sell it to holder 1 + let collateralToDespoit = createTokenAmount(200, usdcDecimals) + let amountToMint = createTokenAmount(1) + let vaultCounter = new BigNumber(await controllerProxy.getAccountVaultCounter(accountOwner1)).plus(1) + const finalMarginPool = (await borrowableMarginPool.isWhitelistedOptionsVault(accountOwner1)) + ? borrowableMarginPool.address + : marginPool.address + + let actionArgs = [ + { + actionType: ActionType.OpenVault, + owner: accountOwner1, + secondAddress: accountOwner1, + asset: ZERO_ADDR, + vaultId: vaultCounter.toNumber(), + amount: '0', + index: '0', + data: ZERO_ADDR, + }, + { + actionType: ActionType.MintShortOption, + owner: accountOwner1, + secondAddress: accountOwner1, + asset: firstShortOtoken.address, + vaultId: vaultCounter.toNumber(), + amount: amountToMint.toString(), + index: '0', + data: ZERO_ADDR, + }, + { + actionType: ActionType.DepositCollateral, + owner: accountOwner1, + secondAddress: accountOwner1, + asset: usdc.address, + vaultId: vaultCounter.toNumber(), + amount: collateralToDespoit, + index: '0', + data: ZERO_ADDR, + }, + ] + await usdc.approve(finalMarginPool, collateralToDespoit, { from: accountOwner1 }) + await controllerProxy.operate(actionArgs, { from: accountOwner1 }) + + expiryTime = new BigNumber(60 * 60 * 24 * 2) // after 1 day + secondShortOtoken = await MockOtoken.new() + await secondShortOtoken.init( + addressBook.address, + weth.address, + usdc.address, + usdc.address, + createTokenAmount(200), + new BigNumber(await time.latest()).plus(expiryTime), + true, + ) + // whitelist otoken to be used in the protocol + await whitelist.whitelistOtoken(secondShortOtoken.address, { from: owner }) + // open new vault, mint naked short, sell it to holder 1 + collateralToDespoit = createTokenAmount(200, usdcDecimals) + amountToMint = createTokenAmount(1) + vaultCounter = new BigNumber(await controllerProxy.getAccountVaultCounter(accountOwner1)).plus(1) + + actionArgs = [ + { + actionType: ActionType.OpenVault, + owner: accountOwner1, + secondAddress: accountOwner1, + asset: ZERO_ADDR, + vaultId: vaultCounter.toNumber(), + amount: '0', + index: '0', + data: ZERO_ADDR, + }, + { + actionType: ActionType.MintShortOption, + owner: accountOwner1, + secondAddress: accountOwner1, + asset: secondShortOtoken.address, + vaultId: vaultCounter.toNumber(), + amount: amountToMint.toString(), + index: '0', + data: ZERO_ADDR, + }, + { + actionType: ActionType.DepositCollateral, + owner: accountOwner1, + secondAddress: accountOwner1, + asset: usdc.address, + vaultId: vaultCounter.toNumber(), + amount: collateralToDespoit, + index: '0', + data: ZERO_ADDR, + }, + ] + await usdc.approve(finalMarginPool, collateralToDespoit, { from: accountOwner1 }) + await controllerProxy.operate(actionArgs, { from: accountOwner1 }) + }) + + it('should settle multiple vaults in one transaction (ATM,OTM)', async () => { + await time.increaseTo(new BigNumber(await secondShortOtoken.expiryTimestamp()).plus(1000).toString()) + const expiry = new BigNumber(await firstShortOtoken.expiryTimestamp()) + const expiry2 = new BigNumber(await secondShortOtoken.expiryTimestamp()) + await oracle.setExpiryPriceFinalizedAllPeiodOver(weth.address, expiry, createTokenAmount(200), true) + await oracle.setExpiryPriceFinalizedAllPeiodOver(usdc.address, expiry, createTokenAmount(1), true) + await oracle.setExpiryPriceFinalizedAllPeiodOver(weth.address, expiry2, createTokenAmount(200), true) + await oracle.setExpiryPriceFinalizedAllPeiodOver(usdc.address, expiry2, createTokenAmount(1), true) + const vaultCounter = new BigNumber(await controllerProxy.getAccountVaultCounter(accountOwner1)) + const finalMarginPool = (await borrowableMarginPool.isWhitelistedOptionsVault(accountOwner1)) + ? borrowableMarginPool.address + : marginPool.address + const actionArgs = [ + { + actionType: ActionType.SettleVault, + owner: accountOwner1, + secondAddress: accountOwner1, + asset: secondShortOtoken.address, + vaultId: vaultCounter.toNumber(), + amount: '0', + index: '0', + data: ZERO_ADDR, + }, + { + actionType: ActionType.SettleVault, + owner: accountOwner1, + secondAddress: accountOwner1, + asset: firstShortOtoken.address, + vaultId: vaultCounter.minus(1).toNumber(), + amount: '0', + index: '0', + data: ZERO_ADDR, + }, + ] + + const payout = createTokenAmount(400, usdcDecimals) + const marginPoolBalanceBefore = new BigNumber(await usdc.balanceOf(finalMarginPool)) + const senderBalanceBefore = new BigNumber(await usdc.balanceOf(accountOwner1)) + + controllerProxy.operate(actionArgs, { from: accountOwner1 }) + + const marginPoolBalanceAfter = new BigNumber(await usdc.balanceOf(finalMarginPool)) + const senderBalanceAfter = new BigNumber(await usdc.balanceOf(accountOwner1)) + + assert.equal( + marginPoolBalanceBefore.minus(marginPoolBalanceAfter).toString(), + payout, + 'Margin pool collateral asset balance mismatch', + ) + assert.equal( + senderBalanceAfter.minus(senderBalanceBefore).toString(), + payout, + 'Sender collateral asset balance mismatch', + ) + }) + }) + }) + + describe('Check if price is finalized', () => { + let expiredOtoken: MockOtokenInstance + let expiry: BigNumber + + before(async () => { + expiry = new BigNumber(await time.latest()) + expiredOtoken = await MockOtoken.new() + // init otoken + await expiredOtoken.init( + addressBook.address, + weth.address, + usdc.address, + usdc.address, + createTokenAmount(200), + new BigNumber(await time.latest()), + true, + ) + + // set finalized + await oracle.setIsFinalized(weth.address, expiry, true) + await oracle.setIsDisputePeriodOver(weth.address, expiry, true) + }) + + it('should return false when price is pushed and dispute period not over yet', async () => { + const priceMock = new BigNumber('200') + + // Mock oracle returned data. + await oracle.setIsLockingPeriodOver(weth.address, expiry, true) + await oracle.setIsDisputePeriodOver(weth.address, expiry, false) + await oracle.setExpiryPrice(weth.address, expiry, priceMock) + + const underlying = await expiredOtoken.underlyingAsset() + const strike = await expiredOtoken.strikeAsset() + const collateral = await expiredOtoken.collateralAsset() + const expiryTimestamp = await expiredOtoken.expiryTimestamp() + + const expectedResult = false + assert.equal( + await controllerProxy.canSettleAssets(underlying, strike, collateral, expiryTimestamp), + expectedResult, + 'Price is not finalized because dispute period is not over yet', + ) + assert.equal( + await controllerProxy.isSettlementAllowed(expiredOtoken.address), + expectedResult, + 'Price is not finalized', + ) + }) + + it('should return true when price is finalized', async () => { + expiredOtoken = await MockOtoken.new() + const expiry = new BigNumber(await time.latest()) + // init otoken + await expiredOtoken.init( + addressBook.address, + weth.address, + usdc.address, + usdc.address, + createTokenAmount(200), + expiry, + true, + ) + + // Mock oracle: dispute periodd over, set price to 200. + const priceMock = new BigNumber('200') + await oracle.setExpiryPriceFinalizedAllPeiodOver(weth.address, expiry, priceMock, true) + await oracle.setExpiryPriceFinalizedAllPeiodOver(usdc.address, expiry, createTokenAmount(1), true) + + const underlying = await expiredOtoken.underlyingAsset() + const strike = await expiredOtoken.strikeAsset() + const collateral = await expiredOtoken.collateralAsset() + const expiryTimestamp = await expiredOtoken.expiryTimestamp() + + const expectedResult = true + assert.equal( + await controllerProxy.canSettleAssets(underlying, strike, collateral, expiryTimestamp), + expectedResult, + 'Price is not finalized', + ) + assert.equal( + await controllerProxy.isSettlementAllowed(expiredOtoken.address), + expectedResult, + 'Price is not finalized', + ) + }) + }) + + describe('Expiry', () => { + it('should return false for non expired otoken', async () => { + const otoken: MockOtokenInstance = await MockOtoken.new() + await otoken.init( + addressBook.address, + weth.address, + usdc.address, + usdc.address, + createTokenAmount(200), + new BigNumber(await time.latest()).plus(60000 * 60000), + true, + ) + + assert.equal(await controllerProxy.hasExpired(otoken.address), false, 'Otoken expiry check mismatch') + }) + + it('should return true for expired otoken', async () => { + // Otoken deployment + const expiredOtoken = await MockOtoken.new() + // init otoken + await expiredOtoken.init( + addressBook.address, + weth.address, + usdc.address, + usdc.address, + createTokenAmount(200), + 1219835219, + true, + ) + + assert.equal(await controllerProxy.hasExpired(expiredOtoken.address), true, 'Otoken expiry check mismatch') + }) + }) + + describe('Call action', () => { + let callTester: CallTesterInstance + + before(async () => { + callTester = await CallTester.new() + }) + + it('should call any arbitrary destination address when restriction is not activated', async () => { + //whitelist callee before call action + await whitelist.whitelistCallee(callTester.address, { from: owner }) + + const actionArgs = [ + { + actionType: ActionType.Call, + owner: ZERO_ADDR, + secondAddress: callTester.address, + asset: ZERO_ADDR, + vaultId: '0', + amount: '0', + index: '0', + data: ZERO_ADDR, + }, + ] + + expectEvent(await controllerProxy.operate(actionArgs, { from: accountOwner1 }), 'CallExecuted', { + from: accountOwner1, + to: callTester.address, + data: ZERO_ADDR, + }) + }) + + it('should revert activating call action restriction from non-owner', async () => { + await expectRevert( + controllerProxy.setCallRestriction(true, { from: random }), + 'Ownable: caller is not the owner', + ) + }) + + it('should revert calling any arbitrary address when call restriction is activated', async () => { + const arbitraryTarget: CallTesterInstance = await CallTester.new() + + const actionArgs = [ + { + actionType: ActionType.Call, + owner: ZERO_ADDR, + secondAddress: arbitraryTarget.address, + asset: ZERO_ADDR, + vaultId: '0', + amount: '0', + index: '0', + data: ZERO_ADDR, + }, + ] + + await expectRevert(controllerProxy.operate(actionArgs, { from: accountOwner1 }), 'C3') + }) + + it('should call whitelisted callee address when restriction is activated', async () => { + // whitelist callee + await whitelist.whitelistCallee(callTester.address, { from: owner }) + + const actionArgs = [ + { + actionType: ActionType.Call, + owner: ZERO_ADDR, + secondAddress: callTester.address, + asset: ZERO_ADDR, + vaultId: '0', + amount: '0', + index: '0', + data: ZERO_ADDR, + }, + ] + + expectEvent(await controllerProxy.operate(actionArgs, { from: accountOwner1 }), 'CallExecuted', { + from: accountOwner1, + to: callTester.address, + data: ZERO_ADDR, + }) + }) + + it('should deactivate call action restriction from owner', async () => { + await controllerProxy.setCallRestriction(false, { from: owner }) + + assert.equal(await controllerProxy.callRestricted(), false, 'Call action restriction deactivation failed') + }) + }) + + describe('Sync vault latest update timestamp', () => { + it('should update vault latest update timestamp', async () => { + const vaultCounter = new BigNumber(await controllerProxy.getAccountVaultCounter(accountOwner1)) + const timestampBefore = new BigNumber( + (await controllerProxy.getVaultWithDetails(accountOwner1, vaultCounter.toNumber()))[2], + ) + + await controllerProxy.sync(accountOwner1, vaultCounter.toNumber(), { from: random }) + + const timestampAfter = new BigNumber( + (await controllerProxy.getVaultWithDetails(accountOwner1, vaultCounter.toNumber()))[2], + ) + assert.isAbove( + timestampAfter.toNumber(), + timestampBefore.toNumber(), + 'Vault latest update timestamp did not sync', + ) + }) + }) + + describe('Interact with Otoken implementation v1.0.0', () => { + let shortOtokenV1: OtokenImplV1Instance + + before(async () => { + const expiryTime = new BigNumber(60 * 60 * 24) // after 1 day + + shortOtokenV1 = await OtokenImplV1.new() + // init otoken + await shortOtokenV1.init( + addressBook.address, + weth.address, + usdc.address, + usdc.address, + createTokenAmount(200), + new BigNumber(await time.latest()).plus(expiryTime), + true, + ) + // whitelist otoken to be used in the protocol + await whitelist.whitelistOtoken(shortOtokenV1.address, { from: owner }) + // open new vault, mint naked short, sell it to holder 1 + const collateralToDespoit = new BigNumber(await shortOtokenV1.strikePrice()).dividedBy(100) + const vaultCounter = new BigNumber(await controllerProxy.getAccountVaultCounter(accountOwner1)).plus(1) + const finalMarginPool = (await borrowableMarginPool.isWhitelistedOptionsVault(accountOwner1)) + ? borrowableMarginPool.address + : marginPool.address + + const amountToMint = createTokenAmount(1) + const actionArgs = [ + { + actionType: ActionType.OpenVault, + owner: accountOwner1, + secondAddress: accountOwner1, + asset: ZERO_ADDR, + vaultId: vaultCounter.toNumber(), + amount: '0', + index: '0', + data: ZERO_ADDR, + }, + { + actionType: ActionType.DepositCollateral, + owner: accountOwner1, + secondAddress: accountOwner1, + asset: usdc.address, + vaultId: vaultCounter.toNumber(), + amount: collateralToDespoit.toNumber(), + index: '0', + data: ZERO_ADDR, + }, + { + actionType: ActionType.MintShortOption, + owner: accountOwner1, + secondAddress: accountOwner1, + asset: shortOtokenV1.address, + vaultId: vaultCounter.toNumber(), + amount: amountToMint, + index: '0', + data: ZERO_ADDR, + }, + ] + await usdc.approve(finalMarginPool, collateralToDespoit, { from: accountOwner1 }) + await controllerProxy.operate(actionArgs, { from: accountOwner1 }) + + //transfer to holder + await shortOtokenV1.transfer(holder1, amountToMint, { from: accountOwner1 }) + }) + + it('should settle v1 Otoken implementation', async () => { + // past time after expiry + await time.increase(60 * 61 * 24) // increase time with one hour in seconds + + const expiry = new BigNumber(await shortOtokenV1.expiryTimestamp()) + await oracle.setExpiryPriceFinalizedAllPeiodOver(weth.address, expiry, createTokenAmount(150), true) + await oracle.setExpiryPriceFinalizedAllPeiodOver(usdc.address, expiry, createTokenAmount(1), true) + const vaultCounter = new BigNumber(await controllerProxy.getAccountVaultCounter(accountOwner1)) + const finalMarginPool = (await borrowableMarginPool.isWhitelistedOptionsVault(accountOwner1)) + ? borrowableMarginPool.address + : marginPool.address + const actionArgs = [ + { + actionType: ActionType.SettleVault, + owner: accountOwner1, + secondAddress: accountOwner1, + asset: shortOtokenV1.address, + vaultId: vaultCounter.toNumber(), + amount: '0', + index: '0', + data: ZERO_ADDR, + }, + ] + + const payout = createTokenAmount(150, usdcDecimals) + const marginPoolBalanceBefore = new BigNumber(await usdc.balanceOf(finalMarginPool)) + const senderBalanceBefore = new BigNumber(await usdc.balanceOf(accountOwner1)) + const proceed = await controllerProxy.getProceed(accountOwner1, vaultCounter) + + assert.equal(payout, proceed.toString()) + + await controllerProxy.operate(actionArgs, { from: accountOwner1 }) + + const marginPoolBalanceAfter = new BigNumber(await usdc.balanceOf(finalMarginPool)) + const senderBalanceAfter = new BigNumber(await usdc.balanceOf(accountOwner1)) + + assert.equal( + marginPoolBalanceBefore.minus(marginPoolBalanceAfter).toString(), + payout.toString(), + 'Margin pool collateral asset balance mismatch', + ) + assert.equal( + senderBalanceAfter.minus(senderBalanceBefore).toString(), + payout.toString(), + 'Sender collateral asset balance mismatch', + ) + }) + + it('should redeem v1 Otoken implementation', async () => { + const redeemArgs = [ + { + actionType: ActionType.Redeem, + owner: ZERO_ADDR, + secondAddress: holder1, + asset: shortOtokenV1.address, + vaultId: '0', + amount: createTokenAmount(1), + index: '0', + data: ZERO_ADDR, + }, + ] + + const expectedPayout = createTokenAmount(50, usdcDecimals) + + const userBalanceBefore = new BigNumber(await usdc.balanceOf(holder1)) + await controllerProxy.operate(redeemArgs, { from: holder1 }) + const userBalanceAfter = new BigNumber(await usdc.balanceOf(holder1)) + assert.equal(userBalanceAfter.minus(userBalanceBefore).toString(), expectedPayout) + }) + }) + + describe('Pause mechanism', () => { + let shortOtoken: MockOtokenInstance + + before(async () => { + const vaultCounterBefore = new BigNumber(await controllerProxy.getAccountVaultCounter(accountOwner1)) + + const expiryTime = new BigNumber(60 * 60) // after 1 hour + shortOtoken = await MockOtoken.new() + // init otoken + await shortOtoken.init( + addressBook.address, + weth.address, + usdc.address, + usdc.address, + createTokenAmount(200), + new BigNumber(await time.latest()).plus(expiryTime), + true, + ) + + // whitelist otoken to be minted + await whitelist.whitelistOtoken(shortOtoken.address, { from: owner }) + + const collateralToDeposit = createTokenAmount(200, usdcDecimals) + const amountToMint = createTokenAmount(1) // mint 1 otoken + const actionArgs = [ + { + actionType: ActionType.OpenVault, + owner: accountOwner1, + secondAddress: accountOwner1, + asset: ZERO_ADDR, + vaultId: vaultCounterBefore.toNumber() + 1, + amount: '0', + index: '0', + data: ZERO_ADDR, + }, + { + actionType: ActionType.MintShortOption, + owner: accountOwner1, + secondAddress: accountOwner1, + asset: shortOtoken.address, + vaultId: vaultCounterBefore.toNumber() + 1, + amount: amountToMint, + index: '0', + data: ZERO_ADDR, + }, + { + actionType: ActionType.DepositCollateral, + owner: accountOwner1, + secondAddress: accountOwner1, + asset: usdc.address, + vaultId: vaultCounterBefore.toNumber() + 1, + amount: collateralToDeposit, + index: '0', + data: ZERO_ADDR, + }, + ] + const finalMarginPool = (await borrowableMarginPool.isWhitelistedOptionsVault(accountOwner1)) + ? borrowableMarginPool.address + : marginPool.address + + await usdc.approve(finalMarginPool, collateralToDeposit, { from: accountOwner1 }) + await controllerProxy.operate(actionArgs, { from: accountOwner1 }) + }) + + it('should revert set pauser address from non-owner', async () => { + await expectRevert( + controllerProxy.setPartialPauser(partialPauser, { from: random }), + 'Ownable: caller is not the owner', + ) + }) + + it('should set pauser address', async () => { + await controllerProxy.setPartialPauser(partialPauser, { from: owner }) + assert.equal(await controllerProxy.partialPauser(), partialPauser, 'pauser address mismatch') + }) + + it('should revert when pausing the system from address other than pauser', async () => { + await expectRevert(controllerProxy.setSystemPartiallyPaused(true, { from: random }), 'C2') + }) + + it('should pause system', async () => { + const stateBefore = await controllerProxy.systemPartiallyPaused() + assert.equal(stateBefore, false, 'System already paused') + + await controllerProxy.setSystemPartiallyPaused(true, { from: partialPauser }) + + const stateAfter = await controllerProxy.systemPartiallyPaused() + assert.equal(stateAfter, true, 'System not paused') + }) + + it('should revert opening a vault when system is partially paused', async () => { + const vaultCounter = new BigNumber(await controllerProxy.getAccountVaultCounter(accountOwner1)) + const actionArgs = [ + { + actionType: ActionType.OpenVault, + owner: accountOwner1, + secondAddress: accountOwner1, + asset: ZERO_ADDR, + vaultId: vaultCounter.toNumber() + 1, + amount: '0', + index: '0', + data: ZERO_ADDR, + }, + ] + await expectRevert(controllerProxy.operate(actionArgs, { from: accountOwner1 }), 'C4') + }) + + it('should revert depositing collateral when system is partially paused', async () => { + const vaultCounter = new BigNumber(await controllerProxy.getAccountVaultCounter(accountOwner1)) + const finalMarginPool = (await borrowableMarginPool.isWhitelistedOptionsVault(accountOwner1)) + ? borrowableMarginPool.address + : marginPool.address + const collateralToDeposit = new BigNumber(await shortOtoken.strikePrice()).dividedBy(1e8) + const actionArgs = [ + { + actionType: ActionType.DepositCollateral, + owner: accountOwner1, + secondAddress: accountOwner1, + asset: usdc.address, + vaultId: vaultCounter.toNumber() + 1, + amount: collateralToDeposit.toNumber(), + index: '0', + data: ZERO_ADDR, + }, + ] + await usdc.approve(finalMarginPool, collateralToDeposit, { from: accountOwner1 }) + await expectRevert(controllerProxy.operate(actionArgs, { from: accountOwner1 }), 'C4') + }) + + it('should revert minting short otoken when system is partially paused', async () => { + const vaultCounter = new BigNumber(await controllerProxy.getAccountVaultCounter(accountOwner1)) + const finalMarginPool = (await borrowableMarginPool.isWhitelistedOptionsVault(accountOwner1)) + ? borrowableMarginPool.address + : marginPool.address + const collateralToDeposit = new BigNumber(await shortOtoken.strikePrice()).dividedBy(1e8) + const actionArgs = [ + { + actionType: ActionType.MintShortOption, + owner: accountOwner1, + secondAddress: accountOwner1, + asset: shortOtoken.address, + vaultId: vaultCounter.toNumber(), + amount: '1', + index: '0', + data: ZERO_ADDR, + }, + { + actionType: ActionType.DepositCollateral, + owner: accountOwner1, + secondAddress: accountOwner1, + asset: usdc.address, + vaultId: vaultCounter.toNumber(), + amount: collateralToDeposit.toNumber(), + index: '0', + data: ZERO_ADDR, + }, + ] + await usdc.approve(finalMarginPool, collateralToDeposit, { from: accountOwner1 }) + await expectRevert(controllerProxy.operate(actionArgs, { from: accountOwner1 }), 'C4') + }) + + it('should revert withdrawing collateral when system is partially paused', async () => { + const vaultCounter = new BigNumber(await controllerProxy.getAccountVaultCounter(accountOwner1)) + const collateralToWithdraw = new BigNumber(await shortOtoken.strikePrice()).dividedBy(100) + const actionArgs = [ + { + actionType: ActionType.WithdrawCollateral, + owner: accountOwner1, + secondAddress: accountOwner1, + asset: usdc.address, + vaultId: vaultCounter.toNumber(), + amount: collateralToWithdraw.toNumber(), + index: '0', + data: ZERO_ADDR, + }, + ] + await expectRevert(controllerProxy.operate(actionArgs, { from: accountOwner1 }), 'C4') + }) + + it('should revert burning short otoken when system is partially paused', async () => { + const vaultCounter = new BigNumber(await controllerProxy.getAccountVaultCounter(accountOwner1)) + const actionArgs = [ + { + actionType: ActionType.BurnShortOption, + owner: accountOwner1, + secondAddress: accountOwner1, + asset: shortOtoken.address, + vaultId: vaultCounter.toNumber(), + amount: '1', + index: '0', + data: ZERO_ADDR, + }, + ] + + await expectRevert(controllerProxy.operate(actionArgs, { from: accountOwner1 }), 'C4') + }) + + it('should settle vault when system is partially paused', async () => { + // past time after expiry + await time.increase(60 * 61) // increase time with one hour in seconds + // set price in Oracle Mock, 150$ at expiry, expire ITM + const expiry = new BigNumber(await shortOtoken.expiryTimestamp()) + await oracle.setExpiryPriceFinalizedAllPeiodOver(weth.address, expiry, createTokenAmount(150), true) + await oracle.setExpiryPriceFinalizedAllPeiodOver(usdc.address, expiry, createTokenAmount(1), true) + + const vaultCounter = new BigNumber(await controllerProxy.getAccountVaultCounter(accountOwner1)) + const finalMarginPool = (await borrowableMarginPool.isWhitelistedOptionsVault(accountOwner1)) + ? borrowableMarginPool.address + : marginPool.address + const actionArgs = [ + { + actionType: ActionType.SettleVault, + owner: accountOwner1, + secondAddress: accountOwner1, + asset: shortOtoken.address, + vaultId: vaultCounter.toNumber(), + amount: '0', + index: '0', + data: ZERO_ADDR, + }, + ] + + const payout = createTokenAmount(150, usdcDecimals) + const marginPoolBalanceBefore = new BigNumber(await usdc.balanceOf(finalMarginPool)) + const senderBalanceBefore = new BigNumber(await usdc.balanceOf(accountOwner1)) + + await controllerProxy.operate(actionArgs, { from: accountOwner1 }) + + const marginPoolBalanceAfter = new BigNumber(await usdc.balanceOf(finalMarginPool)) + const senderBalanceAfter = new BigNumber(await usdc.balanceOf(accountOwner1)) + + assert.equal( + marginPoolBalanceBefore.minus(marginPoolBalanceAfter).toString(), + payout.toString(), + 'Margin pool collateral asset balance mismatch', + ) + assert.equal( + senderBalanceAfter.minus(senderBalanceBefore).toString(), + payout.toString(), + 'Seller collateral asset balance mismatch', + ) + }) + + it('should redeem when system is partially paused', async () => { + const amountToRedeem = createTokenAmount(1) + // transfer to holder + await shortOtoken.transfer(holder1, amountToRedeem, { from: accountOwner1 }) + + const finalMarginPool = (await borrowableMarginPool.isWhitelistedOptionsVault(accountOwner1)) + ? borrowableMarginPool.address + : marginPool.address + + const actionArgs = [ + { + actionType: ActionType.Redeem, + owner: ZERO_ADDR, + secondAddress: holder1, + asset: shortOtoken.address, + vaultId: '0', + amount: amountToRedeem, + index: '0', + data: ZERO_ADDR, + }, + ] + assert.equal(await controllerProxy.hasExpired(shortOtoken.address), true, 'Short otoken is not expired yet') + + const payout = createTokenAmount(50, usdcDecimals) + const marginPoolBalanceBefore = new BigNumber(await usdc.balanceOf(finalMarginPool)) + const senderBalanceBefore = new BigNumber(await usdc.balanceOf(holder1)) + const senderShortBalanceBefore = new BigNumber(await shortOtoken.balanceOf(holder1)) + + await controllerProxy.operate(actionArgs, { from: holder1 }) + + const marginPoolBalanceAfter = new BigNumber(await usdc.balanceOf(finalMarginPool)) + const senderBalanceAfter = new BigNumber(await usdc.balanceOf(holder1)) + const senderShortBalanceAfter = new BigNumber(await shortOtoken.balanceOf(holder1)) + + assert.equal( + marginPoolBalanceBefore.minus(marginPoolBalanceAfter).toString(), + payout.toString(), + 'Margin pool collateral asset balance mismatch', + ) + assert.equal( + senderBalanceAfter.minus(senderBalanceBefore).toString(), + payout.toString(), + 'Sender collateral asset balance mismatch', + ) + assert.equal( + senderShortBalanceBefore.minus(senderShortBalanceAfter).toString(), + amountToRedeem.toString(), + ' Burned short otoken amount mismatch', + ) + }) + }) + + describe('Full Pause', () => { + let shortOtoken: MockOtokenInstance + + before(async () => { + // deactivate pausing mechanism + await controllerProxy.setSystemPartiallyPaused(false, { from: partialPauser }) + + const vaultCounterBefore = new BigNumber(await controllerProxy.getAccountVaultCounter(accountOwner1)) + + const expiryTime = new BigNumber(60 * 60) // after 1 hour + shortOtoken = await MockOtoken.new() + // init otoken + await shortOtoken.init( + addressBook.address, + weth.address, + usdc.address, + usdc.address, + createTokenAmount(200), + new BigNumber(await time.latest()).plus(expiryTime), + true, + ) + + // whitelist otoken to be minted + await whitelist.whitelistOtoken(shortOtoken.address, { from: owner }) + + const collateralToDeposit = createTokenAmount(200, usdcDecimals) + const amountToMint = createTokenAmount(1) // mint 1 otoken + const actionArgs = [ + { + actionType: ActionType.OpenVault, + owner: accountOwner1, + secondAddress: accountOwner1, + asset: ZERO_ADDR, + vaultId: vaultCounterBefore.toNumber() + 1, + amount: '0', + index: '0', + data: ZERO_ADDR, + }, + { + actionType: ActionType.MintShortOption, + owner: accountOwner1, + secondAddress: accountOwner1, + asset: shortOtoken.address, + vaultId: vaultCounterBefore.toNumber() + 1, + amount: amountToMint, + index: '0', + data: ZERO_ADDR, + }, + { + actionType: ActionType.DepositCollateral, + owner: accountOwner1, + secondAddress: accountOwner1, + asset: usdc.address, + vaultId: vaultCounterBefore.toNumber() + 1, + amount: collateralToDeposit, + index: '0', + data: ZERO_ADDR, + }, + ] + const finalMarginPool = (await borrowableMarginPool.isWhitelistedOptionsVault(accountOwner1)) + ? borrowableMarginPool.address + : marginPool.address + + await usdc.approve(finalMarginPool, collateralToDeposit, { from: accountOwner1 }) + await controllerProxy.operate(actionArgs, { from: accountOwner1 }) + }) + + it('should revert set fullPauser address from non-owner', async () => { + await expectRevert( + controllerProxy.setFullPauser(fullPauser, { from: random }), + 'Ownable: caller is not the owner', + ) + }) + + it('should set fullPauser', async () => { + await controllerProxy.setFullPauser(fullPauser, { from: owner }) + assert.equal(await controllerProxy.fullPauser(), fullPauser, 'Full pauser wrong') + }) + + it('should revert when triggering full pause from address other than pauser', async () => { + await expectRevert(controllerProxy.setSystemFullyPaused(true, { from: random }), 'C1') + }) + + it('should trigger full pause', async () => { + const stateBefore = await controllerProxy.systemFullyPaused() + assert.equal(stateBefore, false, 'System already in full pause state') + + await controllerProxy.setSystemFullyPaused(true, { from: fullPauser }) + + const stateAfter = await controllerProxy.systemFullyPaused() + assert.equal(stateAfter, true, 'System not in full pause state') + }) + + it('should revert opening a vault when system is in full pause state', async () => { + const vaultCounter = new BigNumber(await controllerProxy.getAccountVaultCounter(accountOwner1)) + const actionArgs = [ + { + actionType: ActionType.OpenVault, + owner: accountOwner1, + secondAddress: accountOwner1, + asset: ZERO_ADDR, + vaultId: vaultCounter.toNumber() + 1, + amount: '0', + index: '0', + data: ZERO_ADDR, + }, + ] + await expectRevert(controllerProxy.operate(actionArgs, { from: accountOwner1 }), '.') + }) + + it('should revert depositing collateral when system is in full pause state', async () => { + const vaultCounter = new BigNumber(await controllerProxy.getAccountVaultCounter(accountOwner1)) + const finalMarginPool = (await borrowableMarginPool.isWhitelistedOptionsVault(accountOwner1)) + ? borrowableMarginPool.address + : marginPool.address + const collateralToDeposit = new BigNumber(await shortOtoken.strikePrice()).dividedBy(1e8) + const actionArgs = [ + { + actionType: ActionType.DepositCollateral, + owner: accountOwner1, + secondAddress: accountOwner1, + asset: usdc.address, + vaultId: vaultCounter.toNumber() + 1, + amount: collateralToDeposit.toNumber(), + index: '0', + data: ZERO_ADDR, + }, + ] + await usdc.approve(finalMarginPool, collateralToDeposit, { from: accountOwner1 }) + await expectRevert(controllerProxy.operate(actionArgs, { from: accountOwner1 }), 'C5') + }) + + it('should revert minting short otoken when system is in full pause state', async () => { + const vaultCounter = new BigNumber(await controllerProxy.getAccountVaultCounter(accountOwner1)) + const finalMarginPool = (await borrowableMarginPool.isWhitelistedOptionsVault(accountOwner1)) + ? borrowableMarginPool.address + : marginPool.address + const collateralToDeposit = new BigNumber(await shortOtoken.strikePrice()).dividedBy(1e8) + const actionArgs = [ + { + actionType: ActionType.MintShortOption, + owner: accountOwner1, + secondAddress: accountOwner1, + asset: shortOtoken.address, + vaultId: vaultCounter.toNumber(), + amount: '1', + index: '0', + data: ZERO_ADDR, + }, + { + actionType: ActionType.DepositCollateral, + owner: accountOwner1, + secondAddress: accountOwner1, + asset: usdc.address, + vaultId: vaultCounter.toNumber(), + amount: collateralToDeposit.toNumber(), + index: '0', + data: ZERO_ADDR, + }, + ] + await usdc.approve(finalMarginPool, collateralToDeposit, { from: accountOwner1 }) + await expectRevert(controllerProxy.operate(actionArgs, { from: accountOwner1 }), 'C5') + }) + + it('should revert withdrawing collateral when system is in full pause state', async () => { + const vaultCounter = new BigNumber(await controllerProxy.getAccountVaultCounter(accountOwner1)) + const collateralToWithdraw = new BigNumber(await shortOtoken.strikePrice()).dividedBy(1e8) + const actionArgs = [ + { + actionType: ActionType.WithdrawCollateral, + owner: accountOwner1, + secondAddress: accountOwner1, + asset: usdc.address, + vaultId: vaultCounter.toNumber(), + amount: collateralToWithdraw.toNumber(), + index: '0', + data: ZERO_ADDR, + }, + ] + await expectRevert(controllerProxy.operate(actionArgs, { from: accountOwner1 }), 'C5') + }) + + it('should revert burning short otoken when system is in full pause state', async () => { + const vaultCounter = new BigNumber(await controllerProxy.getAccountVaultCounter(accountOwner1)) + const actionArgs = [ + { + actionType: ActionType.BurnShortOption, + owner: accountOwner1, + secondAddress: accountOwner1, + asset: shortOtoken.address, + vaultId: vaultCounter.toNumber(), + amount: '1', + index: '0', + data: ZERO_ADDR, + }, + ] + + await expectRevert(controllerProxy.operate(actionArgs, { from: accountOwner1 }), 'C5') + }) + + it('should revert settling vault when system is in full pause state', async () => { + // past time after expiry + await time.increase(60 * 61) // increase time with one hour in seconds + // set price in Oracle Mock, 150$ at expiry, expire ITM + await oracle.setExpiryPrice( + await shortOtoken.underlyingAsset(), + new BigNumber(await shortOtoken.expiryTimestamp()), + createTokenAmount(150), + ) + // set it as finalized in mock + await oracle.setIsFinalized( + await shortOtoken.underlyingAsset(), + new BigNumber(await shortOtoken.expiryTimestamp()), + true, + ) + await oracle.setIsDisputePeriodOver( + await shortOtoken.underlyingAsset(), + new BigNumber(await shortOtoken.expiryTimestamp()), + true, + ) + + const vaultCounter = new BigNumber(await controllerProxy.getAccountVaultCounter(accountOwner1)) + const actionArgs = [ + { + actionType: ActionType.SettleVault, + owner: accountOwner1, + secondAddress: accountOwner1, + asset: shortOtoken.address, + vaultId: vaultCounter.toNumber(), + amount: '0', + index: '0', + data: ZERO_ADDR, + }, + ] + + await expectRevert(controllerProxy.operate(actionArgs, { from: accountOwner1 }), 'C5') + }) + + it('should revert redeem when system is in full pause state', async () => { + const shortAmountToBurn = new BigNumber('1') + // transfer to holder + await shortOtoken.transfer(holder1, shortAmountToBurn, { from: accountOwner1 }) + + const actionArgs = [ + { + actionType: ActionType.Redeem, + owner: ZERO_ADDR, + secondAddress: holder1, + asset: shortOtoken.address, + vaultId: '0', + amount: shortAmountToBurn.toNumber(), + index: '0', + data: ZERO_ADDR, + }, + ] + + await expectRevert(controllerProxy.operate(actionArgs, { from: accountOwner1 }), 'C5') + }) + }) + + describe('Donate to pool v1', () => { + it('it should donate to margin pool', async () => { + const amountToDonate = createTokenAmount(10, usdcDecimals) + const storedBalanceBefore = new BigNumber(await marginPool.getStoredBalance(usdc.address)) + + await usdc.approve(marginPool.address, amountToDonate, { from: donor }) + await controllerProxy.donate(usdc.address, amountToDonate, { from: donor }) + + const storedBalanceAfter = new BigNumber(await marginPool.getStoredBalance(usdc.address)) + + assert.equal( + storedBalanceAfter.minus(storedBalanceBefore).toString(), + amountToDonate, + 'Donated amount mismatch', + ) + }) + }) + + describe('Donate to borrowable pool', () => { + it('it should donate to margin pool v2', async () => { + const amountToDonate = createTokenAmount(10, usdcDecimals) + const storedBalanceBefore = new BigNumber(await borrowableMarginPool.getStoredBalance(usdc.address)) + + await usdc.approve(borrowableMarginPool.address, amountToDonate, { from: donor }) + await controllerProxy.donateBorrowablePool(usdc.address, amountToDonate, { from: donor }) + + const storedBalanceAfter = new BigNumber(await borrowableMarginPool.getStoredBalance(usdc.address)) + + assert.equal( + storedBalanceAfter.minus(storedBalanceBefore).toString(), + amountToDonate, + 'Donated amount mismatch', + ) + }) + }) + + describe('Refresh configuration', () => { + it('should revert refreshing configuration from address other than owner', async () => { + await expectRevert(controllerProxy.refreshConfiguration({ from: random }), 'Ownable: caller is not the owner') + }) + + it('should refresh configuratiom', async () => { + // update modules + const oracle = await MockOracle.new(addressBook.address, { from: owner }) + const calculator = await MarginCalculator.new(addressBook.address, { from: owner }) + const marginPool = await MarginPool.new(addressBook.address, { from: owner }) + const whitelist = await MockWhitelistModule.new({ from: owner }) + + await addressBook.setOracle(oracle.address) + await addressBook.setMarginCalculator(calculator.address) + await addressBook.setMarginPool(marginPool.address) + await addressBook.setWhitelist(whitelist.address) + + // referesh controller configuration + await controllerProxy.refreshConfiguration() + assert.equal(await controllerProxy.oracle(), oracle.address, 'Oracle address mismatch after refresh') + assert.equal( + await controllerProxy.calculator(), + calculator.address, + 'Calculator address mismatch after refresh', + ) + assert.equal(await controllerProxy.pool(), marginPool.address, 'Oracle address mismatch after refresh') + assert.equal(await controllerProxy.whitelist(), whitelist.address, 'Oracle address mismatch after refresh') + }) + }) + + describe('Execute an invalid action', () => { + it('Should execute transaction with no state updates', async () => { + const vaultCounter = new BigNumber(await controllerProxy.getAccountVaultCounter(accountOwner1)) + const finalMarginPool = (await borrowableMarginPool.isWhitelistedOptionsVault(accountOwner1)) + ? borrowableMarginPool.address + : marginPool.address + assert.isAbove(vaultCounter.toNumber(), 0, 'Account owner have no vault') + + const collateralToDeposit = createTokenAmount(10, usdcDecimals) + const actionArgs = [ + { + actionType: ActionType.InvalidAction, + owner: accountOwner1, + secondAddress: accountOwner1, + asset: usdc.address, + vaultId: vaultCounter.toNumber(), + amount: collateralToDeposit, + index: '0', + data: ZERO_ADDR, + }, + ] + + await usdc.approve(finalMarginPool, collateralToDeposit, { from: accountOwner1 }) + await expectRevert.unspecified(controllerProxy.operate(actionArgs, { from: accountOwner1 })) + }) + }) + }, +) diff --git a/test/unit-tests/controllerNakedMargin.test.ts b/test/unit-tests/controllerNakedMargin.test.ts index 4e443906a..7d2aeabe7 100644 --- a/test/unit-tests/controllerNakedMargin.test.ts +++ b/test/unit-tests/controllerNakedMargin.test.ts @@ -4,6 +4,7 @@ import { MockOracleInstance, MockWhitelistModuleInstance, MarginPoolInstance, + BorrowableMarginPoolInstance, ControllerInstance, AddressBookInstance, OwnedUpgradeabilityProxyInstance, @@ -29,6 +30,7 @@ const MarginCalculator = artifacts.require('MarginCalculator.sol') const MockWhitelistModule = artifacts.require('MockWhitelistModule.sol') const AddressBook = artifacts.require('AddressBook.sol') const MarginPool = artifacts.require('MarginPool.sol') +const BorrowableMarginPool = artifacts.require('BorrowableMarginPool.sol') const Controller = artifacts.require('Controller.sol') const MarginVault = artifacts.require('MarginVault.sol') @@ -61,6 +63,8 @@ contract('Controller: naked margin', ([owner, accountOwner1, liquidator, random] let calculator: MarginCalculatorInstance // margin pool module let marginPool: MarginPoolInstance + // margin pool module v2 + let borrowableMarginPool: BorrowableMarginPoolInstance // whitelist module mock let whitelist: MockWhitelistModuleInstance // addressbook module mock @@ -105,10 +109,16 @@ contract('Controller: naked margin', ([owner, accountOwner1, liquidator, random] calculator = await MarginCalculator.new(oracle.address) // margin pool deployment marginPool = await MarginPool.new(addressBook.address) + // margin pool v2 deployment + borrowableMarginPool = await BorrowableMarginPool.new(addressBook.address) // whitelist module whitelist = await MockWhitelistModule.new() // set margin pool in addressbook await addressBook.setMarginPool(marginPool.address) + // set borrowable margin pool in addressbook + await addressBook.setAddress(web3.utils.soliditySha3('BORROWABLE_POOL'), borrowableMarginPool.address, { + from: owner, + }) // set calculator in addressbook await addressBook.setMarginCalculator(calculator.address) // set oracle in AddressBook @@ -188,6 +198,10 @@ contract('Controller: naked margin', ([owner, accountOwner1, liquidator, random] // open position const vaultCounter = new BigNumber(await controllerProxy.getAccountVaultCounter(accountOwner1)).plus(1) + const finalMarginPool = (await borrowableMarginPool.isWhitelistedOTokenBuyer(accountOwner1)) + ? borrowableMarginPool.address + : marginPool.address + const vaultType = web3.eth.abi.encodeParameter('uint256', 1) // const collateralAmount = createTokenAmount(shortStrike, usdcDecimals) const collateralAmount = await calculator.getNakedMarginRequired( @@ -234,7 +248,7 @@ contract('Controller: naked margin', ([owner, accountOwner1, liquidator, random] data: ZERO_ADDR, }, ] - await usdc.approve(marginPool.address, collateralAmount.toString(), { from: accountOwner1 }) + await usdc.approve(finalMarginPool, collateralAmount.toString(), { from: accountOwner1 }) await controllerProxy.operate(mintArgs, { from: accountOwner1 }) const nakedMarginPool = new BigNumber(await controllerProxy.getNakedPoolBalance(usdc.address)) @@ -304,6 +318,10 @@ contract('Controller: naked margin', ([owner, accountOwner1, liquidator, random] // open position vaultCounter = new BigNumber(await controllerProxy.getAccountVaultCounter(accountOwner1)).plus(1) + const finalMarginPool = (await borrowableMarginPool.isWhitelistedOTokenBuyer(accountOwner1)) + ? borrowableMarginPool.address + : marginPool.address + const vaultType = web3.eth.abi.encodeParameter('uint256', 1) requiredMargin = new BigNumber( await calculator.getNakedMarginRequired( @@ -354,7 +372,7 @@ contract('Controller: naked margin', ([owner, accountOwner1, liquidator, random] const nakedMarginPoolBefore = new BigNumber(await controllerProxy.getNakedPoolBalance(usdc.address)) - await usdc.approve(marginPool.address, requiredMargin.toString(), { from: accountOwner1 }) + await usdc.approve(finalMarginPool, requiredMargin.toString(), { from: accountOwner1 }) await controllerProxy.operate(mintArgs, { from: accountOwner1 }) const nakedMarginPoolAfter = new BigNumber(await controllerProxy.getNakedPoolBalance(usdc.address)) @@ -520,6 +538,10 @@ contract('Controller: naked margin', ([owner, accountOwner1, liquidator, random] // open position vaultCounter = new BigNumber(await controllerProxy.getAccountVaultCounter(accountOwner1)).plus(1) + const finalMarginPool = (await borrowableMarginPool.isWhitelistedOTokenBuyer(accountOwner1)) + ? borrowableMarginPool.address + : marginPool.address + const vaultType = web3.eth.abi.encodeParameter('uint256', 1) requiredMargin = new BigNumber( await calculator.getNakedMarginRequired( @@ -570,7 +592,7 @@ contract('Controller: naked margin', ([owner, accountOwner1, liquidator, random] const nakedMarginPoolBefore = new BigNumber(await controllerProxy.getNakedPoolBalance(weth.address)) - await weth.approve(marginPool.address, requiredMargin.toString(), { from: accountOwner1 }) + await weth.approve(finalMarginPool, requiredMargin.toString(), { from: accountOwner1 }) await controllerProxy.operate(mintArgs, { from: accountOwner1 }) const nakedMarginPoolAfter = new BigNumber(await controllerProxy.getNakedPoolBalance(weth.address)) @@ -737,6 +759,10 @@ contract('Controller: naked margin', ([owner, accountOwner1, liquidator, random] // open position vaultCounter = new BigNumber(await controllerProxy.getAccountVaultCounter(accountOwner1)).plus(1) + const finalMarginPool = (await borrowableMarginPool.isWhitelistedOTokenBuyer(accountOwner1)) + ? borrowableMarginPool.address + : marginPool.address + const vaultType = web3.eth.abi.encodeParameter('uint256', 1) requiredMargin = new BigNumber( await calculator.getNakedMarginRequired( @@ -784,7 +810,7 @@ contract('Controller: naked margin', ([owner, accountOwner1, liquidator, random] data: ZERO_ADDR, }, ] - await usdc.approve(marginPool.address, requiredMargin.toString(), { from: accountOwner1 }) + await usdc.approve(finalMarginPool, requiredMargin.toString(), { from: accountOwner1 }) await controllerProxy.operate(mintArgs, { from: accountOwner1 }) const latestVaultUpdateTimestamp = new BigNumber( @@ -977,6 +1003,10 @@ contract('Controller: naked margin', ([owner, accountOwner1, liquidator, random] it('should revert depositing collateral in vault that that hit naked cap', async () => { const vaultType = web3.eth.abi.encodeParameter('uint256', 1) const vaultCounter = new BigNumber(await controllerProxy.getAccountVaultCounter(accountOwner1)).plus(1) + const finalMarginPool = (await borrowableMarginPool.isWhitelistedOTokenBuyer(accountOwner1)) + ? borrowableMarginPool.address + : marginPool.address + const capAmount = new BigNumber(await controllerProxy.getNakedCap(usdc.address)) const mintArgs = [ @@ -1001,7 +1031,7 @@ contract('Controller: naked margin', ([owner, accountOwner1, liquidator, random] data: ZERO_ADDR, }, ] - await usdc.approve(marginPool.address, capAmount.toString(), { from: accountOwner1 }) + await usdc.approve(finalMarginPool, capAmount.toString(), { from: accountOwner1 }) await expectRevert(controllerProxy.operate(mintArgs, { from: accountOwner1 }), 'C37') }) }) diff --git a/test/unit-tests/payableProxyController.test.ts b/test/unit-tests/payableProxyController.test.ts index 9640773c2..2afe1bb70 100644 --- a/test/unit-tests/payableProxyController.test.ts +++ b/test/unit-tests/payableProxyController.test.ts @@ -6,6 +6,7 @@ import { MockWhitelistModuleInstance, MarginCalculatorInstance, MarginPoolInstance, + BorrowableMarginPoolInstance, ControllerInstance, AddressBookInstance, OwnedUpgradeabilityProxyInstance, @@ -26,6 +27,7 @@ const MarginCalculator = artifacts.require('MarginCalculator.sol') const MockWhitelistModule = artifacts.require('MockWhitelistModule.sol') const AddressBook = artifacts.require('AddressBook.sol') const MarginPool = artifacts.require('MarginPool.sol') +const BorrowableMarginPool = artifacts.require('BorrowableMarginPool.sol') const Controller = artifacts.require('Controller.sol') const MarginVault = artifacts.require('MarginVault.sol') const CalleeAllowanceTester = artifacts.require('CalleeAllowanceTester.sol') @@ -59,6 +61,8 @@ contract('PayableProxyController', ([owner, accountOwner1, holder1, random]) => let calculator: MarginCalculatorInstance // margin pool module let marginPool: MarginPoolInstance + // margin pool v2 module + let borrowableMarginPool: BorrowableMarginPoolInstance // whitelist module mock let whitelist: MockWhitelistModuleInstance // addressbook module mock @@ -84,12 +88,18 @@ contract('PayableProxyController', ([owner, accountOwner1, holder1, random]) => calculator = await MarginCalculator.new(oracle.address) // margin pool deployment marginPool = await MarginPool.new(addressBook.address) + // margin pool v2 deployment + borrowableMarginPool = await BorrowableMarginPool.new(addressBook.address) // whitelist module whitelist = await MockWhitelistModule.new() // callee allowance tester testerCallee = await CalleeAllowanceTester.new(weth.address) // set margin pool in addressbook await addressBook.setMarginPool(marginPool.address) + // set borrowable margin pool in addressbook + await addressBook.setAddress(web3.utils.soliditySha3('BORROWABLE_POOL'), borrowableMarginPool.address, { + from: owner, + }) // set calculator in addressbook await addressBook.setMarginCalculator(calculator.address) // set oracle in AddressBook @@ -152,14 +162,18 @@ contract('PayableProxyController', ([owner, accountOwner1, holder1, random]) => }, ] - const marginPoolBalanceBefore = new BigNumber(await weth.balanceOf(marginPool.address)) + const finalMarginPool = (await borrowableMarginPool.isWhitelistedOTokenBuyer(accountOwner1)) + ? borrowableMarginPool.address + : marginPool.address + + const marginPoolBalanceBefore = new BigNumber(await weth.balanceOf(finalMarginPool)) await payableProxyController.operate(actionArgs, accountOwner1, { from: accountOwner1, value: collateralToDeposit.toString(), }) - const marginPoolBalanceAfter = new BigNumber(await weth.balanceOf(marginPool.address)) + const marginPoolBalanceAfter = new BigNumber(await weth.balanceOf(finalMarginPool)) const vaultAfter = (await controllerProxy.getVaultWithDetails(accountOwner1, vaultCounter))[0] assert.equal( @@ -208,14 +222,18 @@ contract('PayableProxyController', ([owner, accountOwner1, holder1, random]) => }, ] - const marginPoolBalanceBefore = new BigNumber(await weth.balanceOf(marginPool.address)) + const finalMarginPool = (await borrowableMarginPool.isWhitelistedOTokenBuyer(accountOwner1)) + ? borrowableMarginPool.address + : marginPool.address + + const marginPoolBalanceBefore = new BigNumber(await weth.balanceOf(finalMarginPool)) await payableProxyController.operate(actionArgs, accountOwner1, { from: accountOwner1, value: ethToSend.toString(), }) - const marginPoolBalanceAfter = new BigNumber(await weth.balanceOf(marginPool.address)) + const marginPoolBalanceAfter = new BigNumber(await weth.balanceOf(finalMarginPool)) const vaultAfter = (await controllerProxy.getVaultWithDetails(accountOwner1, vaultCounter))[0] assert.equal( @@ -428,7 +446,13 @@ contract('PayableProxyController', ([owner, accountOwner1, holder1, random]) => data: ZERO_ADDR, }, ] - await usdc.approve(marginPool.address, collateralToDeposit, { from: accountOwner1 }) + + const finalMarginPool = (await borrowableMarginPool.isWhitelistedOTokenBuyer(accountOwner1)) + ? borrowableMarginPool.address + : marginPool.address + + await usdc.approve(finalMarginPool, collateralToDeposit, { from: accountOwner1 }) + await payableProxyController.operate(actionArgs, accountOwner1, { from: accountOwner1 }) // transfer minted short otoken to hodler` await shortOtoken.transfer(holder1, amountToMint.toString(), { from: accountOwner1 })