diff --git a/contracts/mainnet/common/math.sol b/contracts/mainnet/common/math.sol index 7ace163d..72ccd542 100644 --- a/contracts/mainnet/common/math.sol +++ b/contracts/mainnet/common/math.sol @@ -44,6 +44,11 @@ contract DSMath { require(y >= 0, "int-overflow"); } + function toUint(int256 x) internal pure returns (uint256) { + require(x >= 0, "int-overflow"); + return uint256(x); + } + function toRad(uint wad) internal pure returns (uint rad) { rad = mul(wad, 10 ** 27); } diff --git a/contracts/mainnet/connectors/notional/events.sol b/contracts/mainnet/connectors/notional/events.sol new file mode 100644 index 00000000..cf37ec1e --- /dev/null +++ b/contracts/mainnet/connectors/notional/events.sol @@ -0,0 +1,119 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.7.6; + +contract Events { + event LogDepositCollateral( + address indexed account, + uint16 currencyId, + bool isUnderlying, + uint256 depositAmount, + uint256 assetCashDeposited + ); + + event LogWithdrawCollateral( + address indexed account, + uint16 currencyId, + bool isUnderlying, + uint256 amountWithdrawn + ); + + event LogClaimNOTE(address indexed account, uint256 notesClaimed); + + event LogRedeemNTokenRaw( + address indexed account, + uint16 currencyId, + bool sellTokenAssets, + uint96 tokensToRedeem, + int256 assetCashChange + ); + + event LogRedeemNTokenWithdraw( + address indexed account, + uint16 currencyId, + uint96 tokensToRedeem, + uint256 amountToWithdraw, + bool redeemToUnderlying + ); + + event LogRedeemNTokenAndDeleverage( + address indexed account, + uint16 currencyId, + uint96 tokensToRedeem, + uint8 marketIndex, + uint88 fCashAmount + ); + + event LogDepositAndMintNToken( + address indexed account, + uint16 currencyId, + bool isUnderlying, + uint256 depositAmount, + int256 nTokenBalanceChange + ); + + event LogMintNTokenFromCash( + address indexed account, + uint16 currencyId, + uint256 cashBalanceToMint, + int256 nTokenBalanceChange + ); + + event LogDepositAndLend( + address indexed account, + uint16 currencyId, + bool isUnderlying, + uint256 depositAmount, + uint8 marketIndex, + uint88 fCashAmount, + uint32 minLendRate + ); + + event LogDepositCollateralBorrowAndWithdraw( + address indexed account, + bool useUnderlying, + uint256 depositAmount, + uint16 borrowCurrencyId, + uint8 marketIndex, + uint88 fCashAmount, + uint32 maxBorrowRate, + bool redeemToUnderlying + ); + + event LogWithdrawLend( + address indexed account, + uint16 currencyId, + uint8 marketIndex, + uint88 fCashAmount, + uint32 maxBorrowRate + ); + + event LogBatchActionRaw(address indexed account); + + event LogMintSNoteFromBPT(address indexed account, uint256 bptAmount); + + event LogMintSNoteFromETH( + address indexed account, + uint256 noteAmount, + uint256 ethAmount, + uint256 minBPT + ); + + event LogMintSNoteFromWETH( + address indexed account, + uint256 noteAmount, + uint256 wethAmount, + uint256 minBPT + ); + + event LogStartCoolDown(address indexed account); + + event LogStopCoolDown(address indexed account); + + event LogRedeemSNote( + address indexed account, + uint256 sNOTEAmount, + uint256 minWETH, + uint256 minNOTE, + bool redeemWETH + ); +} diff --git a/contracts/mainnet/connectors/notional/helpers.sol b/contracts/mainnet/connectors/notional/helpers.sol new file mode 100644 index 00000000..b551eb27 --- /dev/null +++ b/contracts/mainnet/connectors/notional/helpers.sol @@ -0,0 +1,292 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.7.6; +pragma abicoder v2; + +import { Token, NotionalInterface, StakingInterface, BalanceAction, BalanceActionWithTrades, DepositActionType } from "./interface.sol"; +import { Basic } from "../../common/basic.sol"; +import { DSMath } from "../../common/math.sol"; +import { TokenInterface } from "../../common/interfaces.sol"; + +abstract contract Helpers is DSMath, Basic { + uint8 internal constant LEND_TRADE = 0; + uint8 internal constant BORROW_TRADE = 1; + uint256 internal constant INTERNAL_TOKEN_PRECISION = 1e8; + uint256 internal constant ETH_CURRENCY_ID = 1; + uint256 internal constant MAX_DEPOSIT = type(uint256).max; + + /// @dev Contract address is different on Kovan: 0x0EAE7BAdEF8f95De91fDDb74a89A786cF891Eb0e + NotionalInterface internal constant notional = + NotionalInterface(0x1344A36A1B56144C3Bc62E7757377D288fDE0369); + + /// @dev sNOTE contract address + StakingInterface internal constant staking = + StakingInterface(0x38DE42F4BA8a35056b33A746A6b45bE9B1c3B9d2); + + /// @dev sNOTE balancer pool token address + TokenInterface internal constant bpt = + TokenInterface(0x5122E01D819E58BB2E22528c0D68D310f0AA6FD7); + + /// @dev NOTE token address + TokenInterface internal constant note = + TokenInterface(0xCFEAead4947f0705A14ec42aC3D44129E1Ef3eD5); + + /// @dev WETH token address + TokenInterface internal constant weth = + TokenInterface(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2); + + /// @notice Returns the address of the underlying token for a given currency id, + function getAssetOrUnderlyingToken(uint16 currencyId, bool underlying) + internal + view + returns (address) + { + // prettier-ignore + (Token memory assetToken, Token memory underlyingToken) = notional.getCurrency(currencyId); + return + underlying ? underlyingToken.tokenAddress : assetToken.tokenAddress; + } + + function getCashOrNTokenBalance(uint16 currencyId, bool nToken) + internal + view + returns (uint256) + { + // prettier-ignore + ( + int256 cashBalance, + int256 nTokenBalance, + /* int256 lastClaimTime */ + ) = notional.getAccountBalance(currencyId, address(this)); + return toUint(nToken ? nTokenBalance : cashBalance); + } + + function getNTokenRedeemAmount( + uint16 currencyId, + uint96 _tokensToRedeem, + uint256 getId + ) internal returns (uint96 tokensToRedeem) { + tokensToRedeem = toUint96(getUint(getId, _tokensToRedeem)); + if (tokensToRedeem == type(uint96).max) { + tokensToRedeem = toUint96(getCashOrNTokenBalance(currencyId, true)); + } + } + + function toUint96(uint256 value) internal pure returns (uint96) { + require(value <= type(uint96).max, "uint96 value overflow"); + return uint96(value); + } + + function toUint88(uint256 value) internal pure returns (uint88) { + require(value <= type(uint88).max, "uint88 value overflow"); + return uint88(value); + } + + function getMsgValue( + uint16 currencyId, + bool useUnderlying, + uint256 depositAmount + ) internal pure returns (uint256 msgValue) { + msgValue = (currencyId == ETH_CURRENCY_ID && useUnderlying) + ? depositAmount + : 0; + } + + function convertToInternal(uint16 currencyId, uint256 amount) + internal + view + returns (uint256) + { + // If token decimals is greater than INTERNAL_TOKEN_PRECISION then this will truncate + // down to the internal precision. Resulting dust will accumulate to the protocol. + // If token decimals is less than INTERNAL_TOKEN_PRECISION then this will add zeros to the + // end of amount and will not result in dust. + // prettier-ignore + (Token memory assetToken, /* underlyingToken */) = notional.getCurrency(currencyId); + uint256 decimals = toUint(assetToken.decimals); + if (decimals == INTERNAL_TOKEN_PRECISION) return amount; + return div(mul(amount, INTERNAL_TOKEN_PRECISION), decimals); + } + + function encodeLendTrade( + uint8 marketIndex, + uint88 fCashAmount, + uint32 minLendRate + ) internal pure returns (bytes32) { + return + (bytes32(uint256(LEND_TRADE)) << 248) | + (bytes32(uint256(marketIndex)) << 240) | + (bytes32(uint256(fCashAmount)) << 152) | + (bytes32(uint256(minLendRate)) << 120); + } + + function encodeBorrowTrade( + uint8 marketIndex, + uint88 fCashAmount, + uint32 maxBorrowRate + ) internal pure returns (bytes32) { + return + (bytes32(uint256(BORROW_TRADE)) << 248) | + (bytes32(uint256(marketIndex)) << 240) | + (bytes32(uint256(fCashAmount)) << 152) | + (bytes32(uint256(maxBorrowRate)) << 120); + } + + /// @dev Uses getId to set approval for the given token up to the specified deposit + /// amount only + function getDepositAmountAndSetApproval( + uint256 getId, + uint16 currencyId, + bool useUnderlying, + uint256 depositAmount + ) internal returns (uint256) { + depositAmount = getUint(getId, depositAmount); + if (currencyId == ETH_CURRENCY_ID && useUnderlying) { + // No approval required for ETH so we can return the deposit amount + return + depositAmount == MAX_DEPOSIT + ? address(this).balance + : depositAmount; + } + + address tokenAddress = getAssetOrUnderlyingToken( + currencyId, + useUnderlying + ); + + if (depositAmount == MAX_DEPOSIT) { + depositAmount = TokenInterface(tokenAddress).balanceOf( + address(this) + ); + } + approve(TokenInterface(tokenAddress), address(notional), depositAmount); + return depositAmount; + } + + function getBalance(address addr) internal view returns (uint256) { + if (addr == ethAddr) { + return address(this).balance; + } + + return TokenInterface(addr).balanceOf(address(this)); + } + + function getAddress(uint16 currencyId, bool useUnderlying) + internal + view + returns (address) + { + if (currencyId == ETH_CURRENCY_ID && useUnderlying) { + return ethAddr; + } + + return getAssetOrUnderlyingToken(currencyId, useUnderlying); + } + + /// @dev Executes a trade action and sets the balance change to setId + function executeTradeActionWithBalanceChange( + BalanceActionWithTrades[] memory action, + uint256 msgValue, + uint16 currencyId, + bool useUnderlying, + uint256 setId + ) internal { + address tokenAddress; + uint256 balanceBefore; + if (setId != 0) { + tokenAddress = getAddress(currencyId, useUnderlying); + balanceBefore = getBalance(tokenAddress); + } + + notional.batchBalanceAndTradeAction{ value: msgValue }( + address(this), + action + ); + + if (setId != 0) { + uint256 balanceAfter = getBalance(tokenAddress); + setUint(setId, sub(balanceAfter, balanceBefore)); + } + } + + /// @dev Executes a balance action and sets the balance change to setId + function executeActionWithBalanceChange( + BalanceAction[] memory action, + uint256 msgValue, + uint16 currencyId, + bool useUnderlying, + uint256 setId + ) internal { + address tokenAddress; + uint256 balanceBefore; + if (setId != 0) { + tokenAddress = getAddress(currencyId, useUnderlying); + balanceBefore = getBalance(tokenAddress); + } + + notional.batchBalanceAction{ value: msgValue }(address(this), action); + + if (setId != 0) { + uint256 balanceAfter = getBalance(tokenAddress); + setUint(setId, sub(balanceAfter, balanceBefore)); + } + } + + function getDepositCollateralBorrowAndWithdrawActions( + uint16 depositCurrencyId, + DepositActionType depositAction, + uint256 depositAmount, + uint16 borrowCurrencyId, + uint8 marketIndex, + uint88 fCashAmount, + uint32 maxBorrowRate, + bool redeemToUnderlying + ) internal returns (BalanceActionWithTrades[] memory action) { + BalanceActionWithTrades[] memory actions; + bytes32[] memory trades = new bytes32[](1); + trades[0] = encodeBorrowTrade(marketIndex, fCashAmount, maxBorrowRate); + + if (depositCurrencyId == borrowCurrencyId) { + // In this case the account is likely borrowing against newly minted nTokens + // in the same currency. Technically the other deposit actions may work but + // there's no good reason to borrow against cToken collateral + actions = new BalanceActionWithTrades[](1); + actions[0].actionType = depositAction; + actions[0].currencyId = depositCurrencyId; + actions[0].depositActionAmount = depositAmount; + // Withdraw borrowed amount to wallet + actions[0].withdrawEntireCashBalance = true; + actions[0].redeemToUnderlying = redeemToUnderlying; + actions[0].trades = trades; + + return actions; + } + + // This is the more common case that the account is borrowing against + // collateral in a different currency + actions = new BalanceActionWithTrades[](2); + + uint256 depositIndex; + uint256 borrowIndex; + // Notional requires the batch actions to be ordered by currency id + if (depositCurrencyId < borrowCurrencyId) { + depositIndex = 0; + borrowIndex = 1; + } else { + depositIndex = 1; + borrowIndex = 0; + } + + actions[depositIndex].actionType = depositAction; + actions[depositIndex].currencyId = depositCurrencyId; + actions[depositIndex].depositActionAmount = depositAmount; + + actions[borrowIndex].actionType = DepositActionType.None; + actions[borrowIndex].currencyId = borrowCurrencyId; + // Withdraw borrowed amount to wallet + actions[borrowIndex].withdrawEntireCashBalance = true; + actions[borrowIndex].redeemToUnderlying = redeemToUnderlying; + actions[borrowIndex].trades = trades; + + return actions; + } +} diff --git a/contracts/mainnet/connectors/notional/interface.sol b/contracts/mainnet/connectors/notional/interface.sol new file mode 100644 index 00000000..4b712527 --- /dev/null +++ b/contracts/mainnet/connectors/notional/interface.sol @@ -0,0 +1,160 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.7.6; +pragma abicoder v2; + +import { TokenInterface } from "../../common/interfaces.sol"; + +/// @notice Different types of internal tokens +/// - UnderlyingToken: underlying asset for a cToken (except for Ether) +/// - cToken: Compound interest bearing token +/// - cETH: Special handling for cETH tokens +/// - Ether: the one and only +/// - NonMintable: tokens that do not have an underlying (therefore not cTokens) +enum TokenType { + UnderlyingToken, + cToken, + cETH, + Ether, + NonMintable +} + +/// @notice Specifies different deposit actions that can occur during BalanceAction or BalanceActionWithTrades +enum DepositActionType { + // No deposit action + None, + // Deposit asset cash, depositActionAmount is specified in asset cash external precision + DepositAsset, + // Deposit underlying tokens that are mintable to asset cash, depositActionAmount is specified in underlying token + // external precision + DepositUnderlying, + // Deposits specified asset cash external precision amount into an nToken and mints the corresponding amount of + // nTokens into the account + DepositAssetAndMintNToken, + // Deposits specified underlying in external precision, mints asset cash, and uses that asset cash to mint nTokens + DepositUnderlyingAndMintNToken, + // Redeems an nToken balance to asset cash. depositActionAmount is specified in nToken precision. Considered a deposit action + // because it deposits asset cash into an account. If there are fCash residuals that cannot be sold off, will revert. + RedeemNToken, + // Converts specified amount of asset cash balance already in Notional to nTokens. depositActionAmount is specified in + // Notional internal 8 decimal precision. + ConvertCashToNToken +} + +/// @notice Defines a balance action with a set of trades to do as well +struct BalanceActionWithTrades { + DepositActionType actionType; + uint16 currencyId; + uint256 depositActionAmount; + uint256 withdrawAmountInternalPrecision; + bool withdrawEntireCashBalance; + bool redeemToUnderlying; + // Array of tightly packed 32 byte objects that represent trades. See TradeActionType documentation + bytes32[] trades; +} + +/// @notice Defines a balance action for batchAction +struct BalanceAction { + // Deposit action to take (if any) + DepositActionType actionType; + uint16 currencyId; + // Deposit action amount must correspond to the depositActionType, see documentation above. + uint256 depositActionAmount; + // Withdraw an amount of asset cash specified in Notional internal 8 decimal precision + uint256 withdrawAmountInternalPrecision; + // If set to true, will withdraw entire cash balance. Useful if there may be an unknown amount of asset cash + // residual left from trading. + bool withdrawEntireCashBalance; + // If set to true, will redeem asset cash to the underlying token on withdraw. + bool redeemToUnderlying; +} + +struct Token { + // Address of the token + address tokenAddress; + // True if the token has a transfer fee which is used internally to determine + // the proper balance change + bool hasTransferFee; + // Decimal precision of the token as a power of 10 + int256 decimals; + // Type of token, enumerated above + TokenType tokenType; + // Used internally for tokens that have a collateral cap, zero if there is no cap + uint256 maxCollateralBalance; +} + +interface NotionalInterface { + function getCurrency(uint16 currencyId) + external + view + returns (Token memory assetToken, Token memory underlyingToken); + + function getAccountBalance(uint16 currencyId, address account) + external + view + returns ( + int256 cashBalance, + int256 nTokenBalance, + uint256 lastClaimTime + ); + + function depositUnderlyingToken( + address account, + uint16 currencyId, + uint256 amountExternalPrecision + ) external payable returns (uint256); + + function depositAssetToken( + address account, + uint16 currencyId, + uint256 amountExternalPrecision + ) external returns (uint256); + + function withdraw( + uint16 currencyId, + uint88 amountInternalPrecision, + bool redeemToUnderlying + ) external returns (uint256); + + function nTokenClaimIncentives() external returns (uint256); + + function nTokenRedeem( + address redeemer, + uint16 currencyId, + uint96 tokensToRedeem_, + bool sellTokenAssets, + bool acceptResidualAssets + ) external returns (int256); + + function batchBalanceAction( + address account, + BalanceAction[] calldata actions + ) external payable; + + function batchBalanceAndTradeAction( + address account, + BalanceActionWithTrades[] calldata actions + ) external payable; +} + +interface StakingInterface is TokenInterface { + function mintFromETH(uint256 noteAmount, uint256 minBPT) external payable; + + function mintFromWETH( + uint256 noteAmount, + uint256 wethAmount, + uint256 minBPT + ) external; + + function mintFromBPT(uint256 bptAmount) external; + + function startCoolDown() external; + + function stopCoolDown() external; + + function redeem( + uint256 sNOTEAmount, + uint256 minWETH, + uint256 minNOTE, + bool redeemWETH + ) external; +} diff --git a/contracts/mainnet/connectors/notional/main.sol b/contracts/mainnet/connectors/notional/main.sol new file mode 100644 index 00000000..a7b85c76 --- /dev/null +++ b/contracts/mainnet/connectors/notional/main.sol @@ -0,0 +1,813 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.7.6; +pragma abicoder v2; + +/** + * @title Notional + * @dev Fixed Rate Lending and Borrowing + */ + +import { Helpers } from "./helpers.sol"; +import { Events } from "./events.sol"; +import { DepositActionType, BalanceActionWithTrades, BalanceAction } from "./interface.sol"; +import { TokenInterface } from "../../common/interfaces.sol"; + +abstract contract NotionalResolver is Events, Helpers { + /** + * @notice Deposit collateral into Notional, this should only be used for reducing risk of + * liquidation. + * @dev Deposits into Notional are not earning fixed rates, they are earning the cToken + * lending rate. In order to lend at fixed rates use `depositAndLend` + * @param currencyId notional defined currency id to deposit + * @param useUnderlying if true, will accept a deposit in the underlying currency (i.e DAI), if false + * will use the asset currency (i.e. cDAI) + * @param depositAmount amount of tokens to deposit + * @param getId id of depositAmount + * @param setId id to set the value of notional cash deposit increase (denominated in asset cash, i.e. cDAI) + */ + function depositCollateral( + uint16 currencyId, + bool useUnderlying, + uint256 depositAmount, + uint256 getId, + uint256 setId + ) + external + payable + returns (string memory _eventName, bytes memory _eventParam) + { + depositAmount = getDepositAmountAndSetApproval( + getId, + currencyId, + useUnderlying, + depositAmount + ); + + uint256 assetCashDeposited; + if (useUnderlying && currencyId == ETH_CURRENCY_ID) { + assetCashDeposited = notional.depositUnderlyingToken{ + value: depositAmount + }(address(this), currencyId, depositAmount); + } else if (useUnderlying) { + assetCashDeposited = notional.depositUnderlyingToken( + address(this), + currencyId, + depositAmount + ); + } else { + assetCashDeposited = notional.depositAssetToken( + address(this), + currencyId, + depositAmount + ); + } + + setUint(setId, assetCashDeposited); + + _eventName = "LogDepositCollateral(address,uint16,bool,uint256,uint256)"; + _eventParam = abi.encode( + address(this), + currencyId, + useUnderlying, + depositAmount, + assetCashDeposited + ); + } + + /** + * @notice Withdraw collateral from Notional + * @dev This spell allows users to withdraw collateral from Notional + * @param currencyId notional defined currency id to withdraw + * @param redeemToUnderlying if true, will redeem the amount withdrawn to the underlying currency (i.e. DAI), + * if false, will simply withdraw the asset token (i.e. cDAI) + * @param withdrawAmount amount of tokens to withdraw, denominated in asset tokens (i.e. cDAI) + * @param getId id of withdraw amount + * @param setId id to set the value of amount withdrawn, if redeemToUnderlying this amount will be in underlying + * (i.e. DAI), if not redeemToUnderlying this amount will be asset tokens (i.e. cDAI) + */ + function withdrawCollateral( + uint16 currencyId, + bool redeemToUnderlying, + uint256 withdrawAmount, + uint256 getId, + uint256 setId + ) + external + payable + returns (string memory _eventName, bytes memory _eventParam) + { + withdrawAmount = getUint(getId, withdrawAmount); + uint88 amountInternalPrecision = withdrawAmount == type(uint256).max + ? toUint88(getCashOrNTokenBalance(currencyId, false)) + : toUint88(convertToInternal(currencyId, withdrawAmount)); + + uint256 amountWithdrawn = notional.withdraw( + currencyId, + amountInternalPrecision, + redeemToUnderlying + ); + // Sets the amount of tokens withdrawn to address(this), Notional returns this value + // in the native precision of the token that was withdrawn + setUint(setId, amountWithdrawn); + + _eventName = "LogWithdrawCollateral(address,uint16,bool,uint256)"; + _eventParam = abi.encode( + address(this), + currencyId, + redeemToUnderlying, + amountWithdrawn + ); + } + + /** + * @notice Claims NOTE tokens and transfers to the address + * @dev This spell allows users to claim nToken incentives + * @param setId the id to set the balance of NOTE tokens claimed + */ + function claimNOTE(uint256 setId) + external + payable + returns (string memory _eventName, bytes memory _eventParam) + { + uint256 notesClaimed = notional.nTokenClaimIncentives(); + setUint(setId, notesClaimed); + + _eventName = "LogClaimNOTE(address,uint256)"; + _eventParam = abi.encode(address(this), notesClaimed); + } + + /** + * @notice Redeem nTokens allowing for accepting of fCash residuals + * @dev This spell allows users to redeem nTokens even when there are fCash residuals that + * cannot be sold when markets are at extremely high utilization + * @param currencyId notional defined currency id of nToken + * @param sellTokenAssets set to false to accept fCash residuals into portfolio, set to true will + * sell fCash residuals back to cash + * @param tokensToRedeem amount of nTokens to redeem + * @param getId id of amount of tokens to redeem + * @param setId id to set amount of asset cash from redeem + */ + function redeemNTokenRaw( + uint16 currencyId, + bool sellTokenAssets, + uint96 tokensToRedeem, + bool acceptResidualAssets, + uint256 getId, + uint256 setId + ) + external + payable + returns (string memory _eventName, bytes memory _eventParam) + { + tokensToRedeem = getNTokenRedeemAmount( + currencyId, + tokensToRedeem, + getId + ); + + int256 _assetCashChange = notional.nTokenRedeem( + address(this), + currencyId, + tokensToRedeem, + sellTokenAssets, + acceptResidualAssets + ); + + // Floor asset cash change at zero in order to properly set the uint. If the asset cash change is negative + // (this will almost certainly never happen), then no withdraw is possible. + uint256 assetCashChange = _assetCashChange > 0 + ? uint256(_assetCashChange) + : 0; + + setUint(setId, assetCashChange); + + _eventName = "LogRedeemNTokenRaw(address,uint16,bool,uint96,int256)"; + _eventParam = abi.encode( + address(this), + currencyId, + sellTokenAssets, + tokensToRedeem, + assetCashChange + ); + } + + /** + * @notice Redeems nTokens to cash and withdraws the resulting cash + * @dev Also possible to use redeemNTokenRaw and withdrawCollateral to achieve the same + * result but this is more gas efficient, it does it in one call to Notional + * @param currencyId notional defined currency id of nToken + * @param tokensToRedeem amount of nTokens to redeem + * @param amountToWithdraw amount of asset cash to withdraw, if set to uint(-1) then will withdraw the + * entire cash balance in notional + * @param redeemToUnderlying if true, will redeem the asset cash withdrawn to underlying tokens + * @param getId id of amount of tokens to redeem + * @param setId id to set amount of asset cash or underlying tokens withdrawn + */ + function redeemNTokenAndWithdraw( + uint16 currencyId, + uint96 tokensToRedeem, + uint256 amountToWithdraw, + bool redeemToUnderlying, + uint256 getId, + uint256 setId + ) + external + payable + returns (string memory _eventName, bytes memory _eventParam) + { + tokensToRedeem = getNTokenRedeemAmount( + currencyId, + tokensToRedeem, + getId + ); + + BalanceAction[] memory action = new BalanceAction[](1); + action[0].actionType = DepositActionType.RedeemNToken; + action[0].currencyId = currencyId; + action[0].depositActionAmount = tokensToRedeem; + action[0].redeemToUnderlying = redeemToUnderlying; + if (amountToWithdraw == type(uint256).max) { + // This setting will override the withdrawAmountInternalPrecision + action[0].withdrawEntireCashBalance = true; + } else { + action[0].withdrawAmountInternalPrecision = amountToWithdraw; + } + + executeActionWithBalanceChange( + action, + 0, + currencyId, + redeemToUnderlying, + setId + ); + + _eventName = "LogRedeemNTokenWithdraw(address,uint16,uint96,uint256,bool)"; + _eventParam = abi.encode( + address(this), + currencyId, + tokensToRedeem, + amountToWithdraw, + redeemToUnderlying + ); + } + + /** + * @notice Redeems nTokens and uses the cash to repay a borrow. + * @dev When specifying fCashAmount be sure to calculate it such that the account + * has enough cash after redeeming nTokens to pay down the debt. This can be done + * off-chain using the Notional SDK. + * @param currencyId notional defined currency id of nToken + * @param tokensToRedeem amount of nTokens to redeem + * @param marketIndex the market index that references where the account will lend + * @param fCashAmount amount of fCash to lend into the market (this has the effect or repaying + * the borrowed cash at current market rates), the corresponding amount of cash will be taken + * from the account after redeeming nTokens. + * @param minLendRate minimum rate where the user will lend, if the rate is lower will revert + * @param getId id of amount of tokens to redeem + */ + function redeemNTokenAndDeleverage( + uint16 currencyId, + uint96 tokensToRedeem, + uint8 marketIndex, + uint88 fCashAmount, + uint32 minLendRate, + uint256 getId + ) + external + payable + returns (string memory _eventName, bytes memory _eventParam) + { + tokensToRedeem = getNTokenRedeemAmount( + currencyId, + tokensToRedeem, + getId + ); + + BalanceActionWithTrades[] memory action = new BalanceActionWithTrades[]( + 1 + ); + action[0].actionType = DepositActionType.RedeemNToken; + action[0].currencyId = currencyId; + action[0].depositActionAmount = tokensToRedeem; + // Withdraw amount, withdraw cash balance and redeemToUnderlying are all 0 or false + + bytes32[] memory trades = new bytes32[](1); + trades[0] = encodeLendTrade(marketIndex, fCashAmount, minLendRate); + action[0].trades = trades; + + notional.batchBalanceAndTradeAction(address(this), action); + + _eventName = "LogRedeemNTokenAndDeleverage(address,uint16,uint96,uint8,uint88)"; + _eventParam = abi.encode( + address(this), + currencyId, + tokensToRedeem, + marketIndex, + fCashAmount + ); + } + + /** + * @notice Deposit asset or underlying tokens and mint nTokens in a single transaction + * @dev This spell allows users to deposit and mint nTokens (providing liquidity) + * @param currencyId notional defined currency id to deposit + * @param depositAmount amount of tokens to deposit + * @param useUnderlying if true, will accept a deposit in the underlying currency (i.e DAI), if false + * will use the asset currency (i.e. cDAI) + * @param getId id of depositAmount + * @param setId id to set the value of nToken balance change + */ + function depositAndMintNToken( + uint16 currencyId, + uint256 depositAmount, + bool useUnderlying, + uint256 getId, + uint256 setId + ) + external + payable + returns (string memory _eventName, bytes memory _eventParam) + { + depositAmount = getDepositAmountAndSetApproval( + getId, + currencyId, + useUnderlying, + depositAmount + ); + + BalanceAction[] memory action = new BalanceAction[](1); + action[0].actionType = useUnderlying + ? DepositActionType.DepositUnderlyingAndMintNToken + : DepositActionType.DepositAssetAndMintNToken; + action[0].currencyId = currencyId; + action[0].depositActionAmount = depositAmount; + // withdraw amount, withdraw cash and redeem to underlying are all 0 and false + + uint256 nTokenBefore = getCashOrNTokenBalance(currencyId, true); + uint256 msgValue = getMsgValue( + currencyId, + useUnderlying, + depositAmount + ); + + notional.batchBalanceAction{ value: msgValue }(address(this), action); + + uint256 nTokenBalanceChange = sub( + getCashOrNTokenBalance(currencyId, true), + nTokenBefore + ); + + if (setId != 0) { + // Set the amount of nTokens minted + setUint(setId, uint256(nTokenBalanceChange)); + } + + _eventName = "LogDepositAndMintNToken(address,uint16,bool,uint256,int256)"; + _eventParam = abi.encode( + address(this), + currencyId, + useUnderlying, + depositAmount, + nTokenBalanceChange + ); + } + + /** + * @notice Uses existing Notional cash balance (deposits in Notional held as cTokens) and uses them to mint + * nTokens. + * @dev This spell allows users to mint nTokens (providing liquidity) from existing cash balance. + * @param currencyId notional defined currency id of the cash balance + * @param cashBalanceToMint amount of account's cash balance to convert to nTokens + * @param getId id of cash balance + * @param setId id to set the value of nToken increase + */ + function mintNTokenFromCash( + uint16 currencyId, + uint256 cashBalanceToMint, + uint256 getId, + uint256 setId + ) + external + payable + returns (string memory _eventName, bytes memory _eventParam) + { + cashBalanceToMint = getUint(getId, cashBalanceToMint); + if (cashBalanceToMint == type(uint256).max) + cashBalanceToMint = getCashOrNTokenBalance(currencyId, false); + + BalanceAction[] memory action = new BalanceAction[](1); + action[0].actionType = DepositActionType.ConvertCashToNToken; + action[0].currencyId = currencyId; + action[0].depositActionAmount = cashBalanceToMint; + // NOTE: withdraw amount, withdraw cash and redeem to underlying are all 0 and false + + uint256 nTokenBefore = getCashOrNTokenBalance(currencyId, true); + + notional.batchBalanceAction(address(this), action); + + uint256 nTokenBalanceChange = sub( + getCashOrNTokenBalance(currencyId, true), + nTokenBefore + ); + + if (setId != 0) { + // Set the amount of nTokens minted + setUint(setId, uint256(nTokenBalanceChange)); + } + + _eventName = "LogMintNTokenFromCash(address,uint16,uint256,int256)"; + _eventParam = abi.encode( + address(this), + currencyId, + cashBalanceToMint, + nTokenBalanceChange + ); + } + + /** + * @notice Deposits some amount of tokens and lends them in the specified market. This method can also be used to repay a + * borrow early by specifying the corresponding market index of an existing borrow. + * @dev Setting the fCash amount and minLendRate are best calculated using the Notional SDK off chain. They can + * be calculated on chain but there is a significant gas cost to doing so. If there is insufficient depositAmount for the + * fCashAmount specified Notional will revert. In most cases there will be some dust amount of cash left after lending and + * this method will withdraw that dust back to the account. + * @param currencyId notional defined currency id to lend + * @param depositAmount amount of cash to deposit to lend + * @param useUnderlying if true, will accept a deposit in the underlying currency (i.e DAI), if false + * will use the asset currency (i.e. cDAI) + * @param marketIndex the market index to lend to. This is a number from 1 to 7 which corresponds to the tenor + * of the fCash asset to lend. Tenors are described here: https://docs.notional.finance/notional-v2/quarterly-rolls/tenors + * @param fCashAmount amount of fCash for the account to receive, this is equal to how much the account will receive + * at maturity (principal plus interest). + * @param minLendRate the minimum interest rate that the account is willing to lend at, if set to zero the account will accept + * any lending rate + * @param getId returns the deposit amount + */ + function depositAndLend( + uint16 currencyId, + uint256 depositAmount, + bool useUnderlying, + uint8 marketIndex, + uint88 fCashAmount, + uint32 minLendRate, + uint256 getId + ) + external + payable + returns (string memory _eventName, bytes memory _eventParam) + { + depositAmount = getDepositAmountAndSetApproval( + getId, + currencyId, + useUnderlying, + depositAmount + ); + + BalanceActionWithTrades[] memory action = new BalanceActionWithTrades[]( + 1 + ); + action[0].actionType = useUnderlying + ? DepositActionType.DepositUnderlying + : DepositActionType.DepositAsset; + action[0].currencyId = currencyId; + action[0].depositActionAmount = depositAmount; + // Withdraw any residual cash from lending back to the token that was used + action[0].withdrawEntireCashBalance = true; + action[0].redeemToUnderlying = useUnderlying; + + bytes32[] memory trades = new bytes32[](1); + trades[0] = encodeLendTrade(marketIndex, fCashAmount, minLendRate); + action[0].trades = trades; + + uint256 msgValue = getMsgValue( + currencyId, + useUnderlying, + depositAmount + ); + notional.batchBalanceAndTradeAction{ value: msgValue }( + address(this), + action + ); + + _eventName = "LogDepositAndLend(address,uint16,bool,uint256,uint8,uint88,uint32)"; + _eventParam = abi.encode( + address(this), + currencyId, + useUnderlying, + depositAmount, + marketIndex, + fCashAmount, + minLendRate + ); + } + + /** + * @notice Deposits some amount of tokens as collateral and borrows. This can be achieved by combining multiple spells but this + * method is more gas efficient by only making a single call to Notional. + * @dev Setting the fCash amount and maxBorrowRate are best calculated using the Notional SDK off chain. The amount of fCash + * when borrowing is more forgiving compared to lending since generally accounts will over collateralize and dust amounts are + * less likely to cause reverts. The Notional SDK will also provide calculations to tell the user what their LTV is for a given + * borrowing action. + * @param depositCurrencyId notional defined currency id of the collateral to deposit + * @param depositAction one of the following values which will define how the collateral is deposited: + * - None: no collateral will be deposited + * - DepositAsset: deposit amount will be specified in asset tokens (i.e. cTokens) + * - DepositUnderlying: deposit amount will be specified in underlying tokens (i.e. DAI) + * - DepositAssetAndMintNToken: deposit amount will be converted to nTokens + * - DepositUnderlyingAndMintNToken: deposit amount will be converted to nTokens + * + * Technically these two deposit types can be used, but there is not a clear reason why they would be used in combination + * with borrowing: + * - RedeemNToken + * - ConvertCashToNToken + * + * @param depositAmount amount of cash to deposit as collateral + * @param borrowCurrencyId id of the currency to borrow + * @param marketIndex the market index to borrow from. This is a number from 1 to 7 which corresponds to the tenor + * of the fCash asset to borrow. Tenors are described here: https://docs.notional.finance/notional-v2/quarterly-rolls/tenors + * @param fCashAmount amount of fCash for the account to borrow, this is equal to how much the account must pay + * at maturity (principal plus interest). + * @param maxBorrowRate the maximum interest rate that the account is willing to borrow at, if set to zero the account will accept + * any borrowing rate + * @param redeemToUnderlying if true, redeems the borrowed balance from cTokens down to the underlying token before transferring + * to the account + * @param getId returns the collateral deposit amount + * @param setId sets the amount that the account borrowed (i.e. how much of borrowCurrencyId it has received) + */ + function depositCollateralBorrowAndWithdraw( + uint16 depositCurrencyId, + DepositActionType depositAction, + uint256 depositAmount, + uint16 borrowCurrencyId, + uint8 marketIndex, + uint88 fCashAmount, + uint32 maxBorrowRate, + bool redeemToUnderlying, + uint256 getId, + uint256 setId + ) + external + payable + returns (string memory _eventName, bytes memory _eventParam) + { + bool useUnderlying = (depositAction == + DepositActionType.DepositUnderlying || + depositAction == DepositActionType.DepositUnderlyingAndMintNToken); + + depositAmount = getDepositAmountAndSetApproval( + getId, + depositCurrencyId, + useUnderlying, + depositAmount + ); + + BalanceActionWithTrades[] + memory actions = getDepositCollateralBorrowAndWithdrawActions( + depositCurrencyId, + depositAction, + depositAmount, + borrowCurrencyId, + marketIndex, + fCashAmount, + maxBorrowRate, + redeemToUnderlying + ); + + uint256 msgValue = getMsgValue( + depositCurrencyId, + useUnderlying, + depositAmount + ); + executeTradeActionWithBalanceChange( + actions, + msgValue, + borrowCurrencyId, + redeemToUnderlying, + setId + ); + + _eventName = "LogDepositCollateralBorrowAndWithdraw(address,bool,uint256,uint16,uint8,uint88,uint32,bool)"; + _eventParam = abi.encode( + address(this), + useUnderlying, + depositAmount, + borrowCurrencyId, + marketIndex, + fCashAmount, + maxBorrowRate, + redeemToUnderlying + ); + } + + /** + * @notice Allows an account to withdraw from a fixed rate lend by selling the fCash back to the market. Equivalent to + * borrowing from the Notional perspective. + * @dev Setting the fCash amount and maxBorrowRate are best calculated using the Notional SDK off chain. Similar to borrowing, + * setting these amounts are a bit more forgiving since there is no change of reverts due to dust amounts. + * @param currencyId notional defined currency id of the lend asset to withdraw + * @param marketIndex the market index of the fCash asset. This is a number from 1 to 7 which corresponds to the tenor + * of the fCash asset. Tenors are described here: https://docs.notional.finance/notional-v2/quarterly-rolls/tenors + * @param fCashAmount amount of fCash at the marketIndex that should be sold + * @param maxBorrowRate the maximum interest rate that the account is willing to sell fCash at at, if set to zero the + * account will accept any rate + * @param setId sets the amount that the account has received when withdrawing its lend + */ + function withdrawLend( + uint16 currencyId, + uint8 marketIndex, + uint88 fCashAmount, + uint32 maxBorrowRate, + uint256 setId + ) + external + payable + returns (string memory _eventName, bytes memory _eventParam) + { + bool useUnderlying = currencyId != ETH_CURRENCY_ID; + BalanceActionWithTrades[] memory action = new BalanceActionWithTrades[]( + 1 + ); + action[0].actionType = DepositActionType.None; + action[0].currencyId = currencyId; + // Withdraw borrowed amount to wallet + action[0].withdrawEntireCashBalance = true; + action[0].redeemToUnderlying = useUnderlying; + + bytes32[] memory trades = new bytes32[](1); + trades[0] = encodeBorrowTrade(marketIndex, fCashAmount, maxBorrowRate); + action[0].trades = trades; + + executeTradeActionWithBalanceChange( + action, + 0, + currencyId, + useUnderlying, + setId + ); + + _eventName = "LogWithdrawLend(address,uint16,uint8,uint88,uint32)"; + _eventParam = abi.encode( + address(this), + currencyId, + marketIndex, + fCashAmount, + maxBorrowRate + ); + } + + /// @notice Mints sNOTE from the underlying BPT token. + /// @dev Mints sNOTE from the underlying BPT token. + /// @param bptAmount is the amount of BPT to transfer from the msg.sender. + function mintSNoteFromBPT(uint256 bptAmount) + external + payable + returns (string memory _eventName, bytes memory _eventParam) + { + if (bptAmount == type(uint256).max) + bptAmount = bpt.balanceOf(address(this)); + + approve(bpt, address(staking), bptAmount); + + staking.mintFromBPT(bptAmount); + + _eventName = "LogMintSNoteFromBPT(address,uint256)"; + _eventParam = abi.encode(address(this), bptAmount); + } + + /// @notice Mints sNOTE from some amount of NOTE and ETH + /// @dev Mints sNOTE from some amount of NOTE and ETH + /// @param noteAmount amount of NOTE to transfer into the sNOTE contract + /// @param minBPT slippage parameter to prevent front running + function mintSNoteFromETH( + uint256 noteAmount, + uint256 ethAmount, + uint256 minBPT, + uint256 getId + ) + external + payable + returns (string memory _eventName, bytes memory _eventParam) + { + noteAmount = getUint(getId, noteAmount); + if (noteAmount == type(uint256).max) + noteAmount = note.balanceOf(address(this)); + + if (ethAmount == type(uint256).max) ethAmount = address(this).balance; + + approve(note, address(staking), noteAmount); + + staking.mintFromETH{ value: ethAmount }(noteAmount, minBPT); + + _eventName = "LogMintSNoteFromETH(address,uint256,uint256,uint256)"; + _eventParam = abi.encode(address(this), ethAmount, noteAmount, minBPT); + } + + /// @notice Mints sNOTE from some amount of NOTE and WETH + /// @dev Mints sNOTE from some amount of NOTE and WETH + /// @param noteAmount amount of NOTE to transfer into the sNOTE contract + /// @param wethAmount amount of WETH to transfer into the sNOTE contract + /// @param minBPT slippage parameter to prevent front running + function mintSNoteFromWETH( + uint256 noteAmount, + uint256 wethAmount, + uint256 minBPT, + uint256 getId + ) + external + payable + returns (string memory _eventName, bytes memory _eventParam) + { + noteAmount = getUint(getId, noteAmount); + if (noteAmount == type(uint256).max) + noteAmount = note.balanceOf(address(this)); + + if (wethAmount == type(uint256).max) + wethAmount = weth.balanceOf(address(this)); + + approve(note, address(staking), noteAmount); + approve(weth, address(staking), wethAmount); + + staking.mintFromWETH(noteAmount, wethAmount, minBPT); + + _eventName = "LogMintSNoteFromWETH(address,uint256,uint256,uint256)"; + _eventParam = abi.encode(address(this), noteAmount, wethAmount, minBPT); + } + + /// @notice Begins a cool down period for the sender + /// @dev This is required to redeem tokens + function startCoolDown() + external + payable + returns (string memory _eventName, bytes memory _eventParam) + { + staking.startCoolDown(); + + _eventName = "LogStartCoolDown(address)"; + _eventParam = abi.encode(address(this)); + } + + /// @notice Stops a cool down for the sender + /// @dev User must start another cool down period in order to call redeemSNote + function stopCoolDown() + external + payable + returns (string memory _eventName, bytes memory _eventParam) + { + staking.stopCoolDown(); + + _eventName = "LogStopCoolDown(address)"; + _eventParam = abi.encode(address(this)); + } + + /// @notice Redeems some amount of sNOTE to underlying constituent tokens (ETH and NOTE). + /// @dev An account must have passed its cool down expiration before they can redeem + /// @param sNOTEAmount amount of sNOTE to redeem + /// @param minWETH slippage protection for ETH/WETH amount + /// @param minNOTE slippage protection for NOTE amount + /// @param redeemWETH true if redeeming to WETH to ETH + function redeemSNote( + uint256 sNOTEAmount, + uint256 minWETH, + uint256 minNOTE, + bool redeemWETH + ) + external + payable + returns (string memory _eventName, bytes memory _eventParam) + { + if (sNOTEAmount == type(uint256).max) + sNOTEAmount = staking.balanceOf(address(this)); + + staking.redeem(sNOTEAmount, minWETH, minNOTE, redeemWETH); + + _eventName = "LogRedeemSNote(address,uint256,uint256,uint256,bool)"; + _eventParam = abi.encode( + address(this), + sNOTEAmount, + minWETH, + minNOTE, + redeemWETH + ); + } + + /** + * @notice Executes a number of batch actions on the account without getId or setId integration + * @dev This method will allow the user to take almost any action on Notional but does not have any + * getId or setId integration. This can be used to roll lends and borrows forward. + * @param actions a set of BatchActionWithTrades that will be executed for this account + */ + function batchActionRaw(BalanceActionWithTrades[] memory actions) + external + payable + returns (string memory _eventName, bytes memory _eventParam) + { + notional.batchBalanceAndTradeAction(address(this), actions); + + _eventName = "LogBatchActionRaw(address)"; + _eventParam = abi.encode(address(this)); + } +} + +contract ConnectV2Notional is NotionalResolver { + string public name = "Notional-v1.1"; +} diff --git a/test/mainnet/notional/notional.contracts.ts b/test/mainnet/notional/notional.contracts.ts new file mode 100644 index 00000000..54809b49 --- /dev/null +++ b/test/mainnet/notional/notional.contracts.ts @@ -0,0 +1,116 @@ + +const NOTIONAL_CONTRACT_ADDRESS = '0x1344A36A1B56144C3Bc62E7757377D288fDE0369'; +const NOTIONAL_CONTRACT_ABI = [ + { + "inputs": [ + { + "internalType": "uint16", + "name": "currencyId", + "type": "uint16" + }, + { + "internalType": "address", + "name": "account", + "type": "address" + } + ], + "name": "getAccountBalance", + "outputs": [ + { + "internalType": "int256", + "name": "cashBalance", + "type": "int256" + }, + { + "internalType": "int256", + "name": "nTokenBalance", + "type": "int256" + }, + { + "internalType": "uint256", + "name": "lastClaimTime", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "account", + "type": "address" + } + ], + "name": "getAccountPortfolio", + "outputs": [ + { + "components": [ + { + "internalType": "uint256", + "name": "currencyId", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "maturity", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "assetType", + "type": "uint256" + }, + { + "internalType": "int256", + "name": "notional", + "type": "int256" + }, + { + "internalType": "uint256", + "name": "storageSlot", + "type": "uint256" + }, + { + "internalType": "enum AssetStorageState", + "name": "storageState", + "type": "uint8" + } + ], + "internalType": "struct PortfolioAsset[]", + "name": "", + "type": "tuple[]" + } + ], + "stateMutability": "view", + "type": "function" + } +]; + +const SNOTE_CONTRACT_ADDRESS = '0x38de42f4ba8a35056b33a746a6b45be9b1c3b9d2'; + +const WETH_TOKEN_ADDRESS = "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2"; +const DAI_TOKEN_ADDRESS = "0x6B175474E89094C44Da98b954EedeAC495271d0F"; +const CDAI_TOKEN_ADDRESS = "0x5d3a536E4D6DbD6114cc1Ead35777bAB948E3643"; +const CETH_TOKEN_ADDRESS = "0x4ddc2d193948926d02f9b1fe9e1daa0718270ed5"; +const BPT_TOKEN_ADDRESS = "0x5122E01D819E58BB2E22528c0D68D310f0AA6FD7"; +const NOTE_TOKEN_ADDRESS = "0xCFEAead4947f0705A14ec42aC3D44129E1Ef3eD5"; +const ERC20_TOKEN_ABI = [ + "function transfer(address _to, uint256 _value) public returns (bool success)", + "function balanceOf(address account) external view returns (uint256)", + "function approve(address spender, uint256 amount) external returns (bool)", +]; + +export default { + NOTIONAL_CONTRACT_ADDRESS, + NOTIONAL_CONTRACT_ABI, + SNOTE_CONTRACT_ADDRESS, + WETH_TOKEN_ADDRESS, + BPT_TOKEN_ADDRESS, + DAI_TOKEN_ADDRESS, + CDAI_TOKEN_ADDRESS, + CETH_TOKEN_ADDRESS, + NOTE_TOKEN_ADDRESS, + ERC20_TOKEN_ABI +}; diff --git a/test/mainnet/notional/notional.helpers.ts b/test/mainnet/notional/notional.helpers.ts new file mode 100644 index 00000000..d5d04bfa --- /dev/null +++ b/test/mainnet/notional/notional.helpers.ts @@ -0,0 +1,332 @@ +import { BigNumber } from "ethers"; +import { encodeSpells } from "../../../scripts/tests/encodeSpells" + +const depositCollteral = async ( + dsa: any, + authority: any, + referrer: any, + currencyId: number, + amount: BigNumber, + underlying: boolean +) => { + const spells = [ + { + connector: "NOTIONAL-TEST-A", + method: "depositCollateral", + args: [currencyId, underlying, amount, 0, 0] + } + ]; + + const tx = await dsa.connect(authority).cast(...encodeSpells(spells), referrer.address); + await tx.wait() +}; + +const depositAndMintNToken = async ( + dsa: any, + authority: any, + referrer: any, + currencyId: number, + amount: BigNumber, + underlying: boolean +) => { + const spells = [ + { + connector: "NOTIONAL-TEST-A", + method: "depositAndMintNToken", + args: [currencyId, amount, underlying, 0, 0] + } + ]; + + const tx = await dsa.connect(authority).cast(...encodeSpells(spells), referrer.address); + await tx.wait() +} + +const depositAndLend = async ( + dsa: any, + authority: any, + referrer: any, + currencyId: number, + underlying: boolean, + amount: BigNumber, + market: number, + fcash: BigNumber +) => { + const spells = [ + { + connector: "NOTIONAL-TEST-A", + method: "depositAndLend", + args: [currencyId, amount, underlying, market, fcash, 0, 0] + } + ]; + + const tx = await dsa.connect(authority).cast(...encodeSpells(spells), referrer.address); + await tx.wait() +}; + +const withdrawCollateral = async ( + dsa: any, + authority: any, + referrer: any, + currencyId: number, + amount: BigNumber, + underlying: boolean +) => { + const spells = [ + { + connector: "NOTIONAL-TEST-A", + method: "withdrawCollateral", + args: [currencyId, underlying, amount, 0, 0] + } + ]; + + const tx = await dsa.connect(authority).cast(...encodeSpells(spells), referrer.address); + await tx.wait() +}; + +const redeemNTokenRaw = async ( + dsa: any, + authority: any, + referrer: any, + currencyId: number, + sellTokenAssets: boolean, + tokensToRedeem: BigNumber +) => { + const spells = [ + { + connector: "NOTIONAL-TEST-A", + method: "redeemNTokenRaw", + args: [currencyId, sellTokenAssets, tokensToRedeem, false, 0, 0] + } + ]; + + const tx = await dsa.connect(authority).cast(...encodeSpells(spells), referrer.address); + await tx.wait() +}; + +const redeemNTokenAndWithdraw = async ( + dsa: any, + authority: any, + referrer: any, + currencyId: number, + tokensToRedeem: BigNumber, + amountToWithdraw: BigNumber, + redeemToUnderlying: boolean +) => { + const spells = [ + { + connector: "NOTIONAL-TEST-A", + method: "redeemNTokenAndWithdraw", + args: [currencyId, tokensToRedeem, amountToWithdraw, redeemToUnderlying, 0, 0] + } + ]; + + const tx = await dsa.connect(authority).cast(...encodeSpells(spells), referrer.address); + await tx.wait() +}; + +const redeemNTokenAndDeleverage = async ( + dsa: any, + authority: any, + referrer: any, + currencyId: number, + tokensToRedeem: BigNumber, + marketIndex: number, + fCashAmount: BigNumber +) => { + const spells = [ + { + connector: "NOTIONAL-TEST-A", + method: "redeemNTokenAndDeleverage", + args: [currencyId, tokensToRedeem, marketIndex, fCashAmount, 0, 0] + } + ]; + + const tx = await dsa.connect(authority).cast(...encodeSpells(spells), referrer.address); + await tx.wait() +}; + +const depositCollateralBorrowAndWithdraw = async ( + dsa: any, + authority: any, + referrer: any, + depositCurrencyId: number, + depositType: number, + depositAmount: BigNumber, + borrowCurrencyId: number, + marketIndex: number, + fCashAmount: BigNumber, + redeedmUnderlying: boolean +) => { + const spells = [ + { + connector: "NOTIONAL-TEST-A", + method: "depositCollateralBorrowAndWithdraw", + args: [ + depositCurrencyId, + depositType, + depositAmount, + borrowCurrencyId, + marketIndex, + fCashAmount, + 0, + redeedmUnderlying, + 0, + 0 + ] + } + ]; + + const tx = await dsa.connect(authority).cast(...encodeSpells(spells), referrer.address); + await tx.wait() +}; + +const withdrawLend = async ( + dsa: any, + authority: any, + referrer: any, + currencyId: number, + marketIndex: number, + fCashAmount: BigNumber +) => { + const spells = [ + { + connector: "NOTIONAL-TEST-A", + method: "withdrawLend", + args: [currencyId, marketIndex, fCashAmount, 0, 0] + } + ]; + + const tx = await dsa.connect(authority).cast(...encodeSpells(spells), referrer.address); + await tx.wait() +}; + +const mintSNoteFromETH = async ( + dsa: any, + authority: any, + referrer: any, + noteAmount: BigNumber, + ethAmount: BigNumber, + minBPT: BigNumber +) => { + const spells = [ + { + connector: "NOTIONAL-TEST-A", + method: "mintSNoteFromETH", + args: [noteAmount, ethAmount, minBPT, 0] + } + ] + + const tx = await dsa.connect(authority).cast(...encodeSpells(spells), referrer.address); + await tx.wait() +} + +const mintSNoteFromWETH = async ( + dsa: any, + authority: any, + referrer: any, + noteAmount: BigNumber, + wethAmount: BigNumber, + minBPT: BigNumber +) => { + const spells = [ + { + connector: "NOTIONAL-TEST-A", + method: "mintSNoteFromWETH", + args: [noteAmount, wethAmount, minBPT, 0] + } + ] + + const tx = await dsa.connect(authority).cast(...encodeSpells(spells), referrer.address); + await tx.wait() +} + +const mintSNoteFromBPT = async ( + dsa: any, + authority: any, + referrer: any, + bptAmount: BigNumber +) => { + const spells = [ + { + connector: "NOTIONAL-TEST-A", + method: "mintSNoteFromBPT", + args: [bptAmount] + } + ] + + const tx = await dsa.connect(authority).cast(...encodeSpells(spells), referrer.address); + await tx.wait() +} + +const startCoolDown = async ( + dsa: any, + authority: any, + referrer: any +) => { + const spells = [ + { + connector: "NOTIONAL-TEST-A", + method: "startCoolDown", + args: [] + } + ] + + const tx = await dsa.connect(authority).cast(...encodeSpells(spells), referrer.address); + await tx.wait() +} + +const stopCoolDown = async ( + dsa: any, + authority: any, + referrer: any +) => { + const spells = [ + { + connector: "NOTIONAL-TEST-A", + method: "stopCoolDown", + args: [] + } + ] + + const tx = await dsa.connect(authority).cast(...encodeSpells(spells), referrer.address); + await tx.wait() +} + +const redeemSNote = async ( + dsa: any, + authority: any, + referrer: any, + sNOTEAmount: BigNumber, + minWETH: BigNumber, + minNOTE: BigNumber, + redeemWETH: boolean +) => { + const spells = [ + { + connector: "NOTIONAL-TEST-A", + method: "redeemSNote", + args: [sNOTEAmount, minWETH, minNOTE, redeemWETH] + } + ] + + const tx = await dsa.connect(authority).cast(...encodeSpells(spells), referrer.address); + await tx.wait() +} + +export default { + depositCollteral, + depositAndMintNToken, + depositAndLend, + withdrawCollateral, + withdrawLend, + redeemNTokenRaw, + redeemNTokenAndWithdraw, + redeemNTokenAndDeleverage, + depositCollateralBorrowAndWithdraw, + mintSNoteFromETH, + mintSNoteFromWETH, + mintSNoteFromBPT, + startCoolDown, + stopCoolDown, + redeemSNote +}; diff --git a/test/mainnet/notional/notional.test.ts b/test/mainnet/notional/notional.test.ts new file mode 100644 index 00000000..304ee253 --- /dev/null +++ b/test/mainnet/notional/notional.test.ts @@ -0,0 +1,513 @@ +import { expect } from "chai"; +import hre from "hardhat"; +const { waffle, ethers } = hre; +const { provider, deployContract } = waffle + +import { deployAndEnableConnector } from "../../../scripts/tests/deployAndEnableConnector"; +import { buildDSAv2 } from "../../../scripts/tests/buildDSAv2" +import { getMasterSigner } from "../../../scripts/tests/getMasterSigner" +import { addresses } from "../../../scripts/tests/mainnet/addresses"; +import { abis } from "../../../scripts/constant/abis"; +import contracts from "./notional.contracts"; +import helpers from "./notional.helpers"; + +import { ConnectV2Notional__factory } from "../../../typechain"; +import { BigNumber } from "ethers"; + +const DAI_WHALE = "0x6dfaf865a93d3b0b5cfd1b4db192d1505676645b"; +const CDAI_WHALE = "0x33b890d6574172e93e58528cd99123a88c0756e9"; +const ETH_WHALE = "0x7D24796f7dDB17d73e8B1d0A3bbD103FBA2cb2FE"; +const CETH_WHALE = "0x1a1cd9c606727a7400bb2da6e4d5c70db5b4cade"; +const WETH_WHALE = "0x6555e1cc97d3cba6eaddebbcd7ca51d75771e0b8"; +const BPT_WHALE = "0x38de42f4ba8a35056b33a746a6b45be9b1c3b9d2"; +const MaxUint96 = BigNumber.from("0xffffffffffffffffffffffff"); +const DEPOSIT_ASSET = 1; +const DEPOSIT_UNDERLYING = 2; +const DEPOSIT_ASSET_MINT_NTOKEN = 3; +const DEPOSIT_UNDERLYING_MINT_NTOKEN = 4; +const ETH_ID = 1; +const DAI_ID = 2; +const MARKET_3M = 1; + +describe("Notional", function () { + const connectorName = "NOTIONAL-TEST-A" + + let dsaWallet0: any; + let masterSigner: any; + let instaConnectorsV2: any; + let connector: any; + let notional: any; + let snote: any; + let daiToken: any; + let cdaiToken: any; + let cethToken: any; + let wethToken: any; + let bptToken: any; + let noteToken: any; + let daiWhale: any; + let cdaiWhale: any; + let cethWhale: any; + let wethWhale: any; + let bptWhale: any; + + const wallets = provider.getWallets() + const [wallet0, wallet1, wallet2, wallet3] = wallets + beforeEach(async () => { + await hre.network.provider.request({ + method: "hardhat_reset", + params: [ + { + forking: { + //@ts-ignore + jsonRpcUrl: hre.config.networks.hardhat.forking.url, + blockNumber: 14483893, + }, + }, + ], + }); + await hre.network.provider.request({ + method: "hardhat_impersonateAccount", + params: [DAI_WHALE] + }) + await hre.network.provider.request({ + method: "hardhat_impersonateAccount", + params: [CDAI_WHALE] + }) + await hre.network.provider.request({ + method: "hardhat_impersonateAccount", + params: [ETH_WHALE] + }) + await hre.network.provider.request({ + method: "hardhat_impersonateAccount", + params: [CETH_WHALE] + }) + await hre.network.provider.request({ + method: "hardhat_impersonateAccount", + params: [WETH_WHALE] + }) + await hre.network.provider.request({ + method: "hardhat_impersonateAccount", + params: [BPT_WHALE] + }) + + masterSigner = await getMasterSigner() + instaConnectorsV2 = await ethers.getContractAt(abis.core.connectorsV2, addresses.core.connectorsV2); + connector = await deployAndEnableConnector({ + connectorName, + contractArtifact: ConnectV2Notional__factory, + signer: masterSigner, + connectors: instaConnectorsV2 + }) + notional = new ethers.Contract( + contracts.NOTIONAL_CONTRACT_ADDRESS, + contracts.NOTIONAL_CONTRACT_ABI, + ethers.provider + ); + snote = new ethers.Contract( + contracts.SNOTE_CONTRACT_ADDRESS, + contracts.ERC20_TOKEN_ABI, + ethers.provider + ) + daiToken = new ethers.Contract( + contracts.DAI_TOKEN_ADDRESS, + contracts.ERC20_TOKEN_ABI, + ethers.provider + ); + daiWhale = await ethers.getSigner(DAI_WHALE); + cdaiToken = new ethers.Contract( + contracts.CDAI_TOKEN_ADDRESS, + contracts.ERC20_TOKEN_ABI, + ethers.provider + ); + cdaiWhale = await ethers.getSigner(CDAI_WHALE); + cethToken = new ethers.Contract( + contracts.CETH_TOKEN_ADDRESS, + contracts.ERC20_TOKEN_ABI, + ethers.provider + ); + cethWhale = await ethers.getSigner(CETH_WHALE); + wethToken = new ethers.Contract( + contracts.WETH_TOKEN_ADDRESS, + contracts.ERC20_TOKEN_ABI, + ethers.provider + ); + wethWhale = await ethers.getSigner(WETH_WHALE); + bptToken = new ethers.Contract( + contracts.BPT_TOKEN_ADDRESS, + contracts.ERC20_TOKEN_ABI, + ethers.provider + ); + bptWhale = await ethers.getSigner(BPT_WHALE); + noteToken = new ethers.Contract( + contracts.NOTE_TOKEN_ADDRESS, + contracts.ERC20_TOKEN_ABI, + ethers.provider + ) + dsaWallet0 = await buildDSAv2(wallet0.address); + }); + + describe("Deposit Tests", function () { + it("test_deposit_ETH_underlying", async function () { + await wallet0.sendTransaction({ + to: dsaWallet0.address, + value: ethers.utils.parseEther("10") + }); + const depositAmount = ethers.utils.parseEther("1"); + await helpers.depositCollteral(dsaWallet0, wallet0, wallet1, ETH_ID, depositAmount, true); + const bal = await notional.callStatic.getAccountBalance(ETH_ID, dsaWallet0.address); + // balance in internal asset precision + expect(bal[0], "expect at least 49 cETH").to.be.gte(ethers.utils.parseUnits("4900000000", 0)); + expect(bal[1], "expect 0 nETH").to.be.equal(ethers.utils.parseUnits("0", 0)); + }); + + it("test_deposit_ETH_asset", async function () { + const depositAmount = ethers.utils.parseUnits("1", 8); + await cethToken.connect(cethWhale).transfer(dsaWallet0.address, depositAmount); + await helpers.depositCollteral(dsaWallet0, wallet0, wallet1, ETH_ID, depositAmount, false); + const bal = await notional.callStatic.getAccountBalance(ETH_ID, dsaWallet0.address); + // balance in internal asset precision + expect(bal[0], "expect at least 1 cETH").to.be.gte(ethers.utils.parseUnits("100000000", 0)); + expect(bal[1], "expect 0 nETH").to.be.equal(ethers.utils.parseUnits("0", 0)); + }); + + it("test_deposit_DAI_underlying", async function () { + const depositAmount = ethers.utils.parseUnits("1000", 18); + await daiToken.connect(daiWhale).transfer(dsaWallet0.address, depositAmount); + await helpers.depositCollteral(dsaWallet0, wallet0, wallet1, DAI_ID, depositAmount, true); + const bal = await notional.callStatic.getAccountBalance(DAI_ID, dsaWallet0.address); + // balance in internal asset precision + expect(bal[0], "expect at least 45000 cDAI").to.be.gte(ethers.utils.parseUnits("4500000000000", 0)); + expect(bal[1], "expect 0 nDAI").to.be.equal(ethers.utils.parseUnits("0", 0)); + }); + + it("test_deposit_DAI_asset", async function () { + const depositAmount = ethers.utils.parseUnits("1000", 8); + await cdaiToken.connect(cdaiWhale).transfer(dsaWallet0.address, depositAmount); + await helpers.depositCollteral(dsaWallet0, wallet0, wallet1, DAI_ID, depositAmount, false); + const bal = await notional.callStatic.getAccountBalance(DAI_ID, dsaWallet0.address); + // balance in internal asset precision + expect(bal[0], "expect at least 1000 cDAI").to.be.gte(ethers.utils.parseUnits("100000000000", 0)); + expect(bal[1], "expect 0 nDAI").to.be.equal(ethers.utils.parseUnits("0", 0)); + }); + + it("test_deposit_ETH_underlying_and_mint_ntoken", async function () { + await wallet0.sendTransaction({ + to: dsaWallet0.address, + value: ethers.utils.parseEther("10") + }); + const depositAmount = ethers.utils.parseEther("1"); + await helpers.depositAndMintNToken(dsaWallet0, wallet0, wallet1, ETH_ID, depositAmount, true); + const bal = await notional.callStatic.getAccountBalance(ETH_ID, dsaWallet0.address); + expect(bal[0], "expect 0 balance").to.be.equal(ethers.utils.parseUnits("0", 0)); + expect(bal[1], "expect at least 49 nETH").to.be.gte(ethers.utils.parseUnits("4900000000", 0)); + }); + }); + + describe("Lend Tests", function () { + it("test_deposit_ETH_underlying_and_lend", async function () { + await wallet0.sendTransaction({ + to: dsaWallet0.address, + value: ethers.utils.parseEther("10") + }); + const depositAmount = ethers.utils.parseEther("10"); + const fcash = ethers.utils.parseUnits("9", 8); + await helpers.depositAndLend(dsaWallet0, wallet0, wallet1, ETH_ID, true, depositAmount, MARKET_3M, fcash); + const portfolio = await notional.getAccountPortfolio(dsaWallet0.address); + expect(portfolio.length, "expect 1 lending position").to.be.equal(1); + expect(portfolio[0][3], "expect 9 fETH").to.be.gte(ethers.utils.parseUnits("900000000", 0)); + }); + + it("test_deposit_ETH_asset_and_lend", async function () { + const depositAmount = ethers.utils.parseUnits("1", 8); + await cethToken.connect(cethWhale).transfer(dsaWallet0.address, depositAmount); + const fcash = ethers.utils.parseUnits("0.01", 8); + await helpers.depositAndLend(dsaWallet0, wallet0, wallet1, ETH_ID, false, depositAmount, MARKET_3M, fcash); + const portfolio = await notional.getAccountPortfolio(dsaWallet0.address); + expect(portfolio.length, "expect 1 lending position").to.be.equal(1); + expect(portfolio[0][3], "expect 0.01 fETH").to.be.gte(ethers.utils.parseUnits("1000000", 0)); + }); + + it("test_deposit_DAI_underlying_and_lend", async function () { + const depositAmount = ethers.utils.parseUnits("1000", 18); + await daiToken.connect(daiWhale).transfer(dsaWallet0.address, depositAmount); + const fcash = ethers.utils.parseUnits("100", 8); + await helpers.depositAndLend(dsaWallet0, wallet0, wallet1, DAI_ID, true, depositAmount, MARKET_3M, fcash); + const portfolio = await notional.getAccountPortfolio(dsaWallet0.address); + expect(portfolio.length, "expect 1 lending position").to.be.equal(1); + expect(portfolio[0][3], "expect 100 fDAI").to.be.gte(ethers.utils.parseUnits("10000000000", 0)); + }); + + it("test_deposit_DAI_asset_and_lend", async function () { + const depositAmount = ethers.utils.parseUnits("1000", 8); + await cdaiToken.connect(cdaiWhale).transfer(dsaWallet0.address, depositAmount); + const fcash = ethers.utils.parseUnits("10", 8); + await helpers.depositAndLend(dsaWallet0, wallet0, wallet1, DAI_ID, false, depositAmount, MARKET_3M, fcash); + const portfolio = await notional.getAccountPortfolio(dsaWallet0.address); + expect(portfolio.length, "expect 1 lending position").to.be.equal(1); + expect(portfolio[0][3], "expect 10 fDAI").to.be.gte(ethers.utils.parseUnits("1000000000", 0)); + }); + + it("test_withdraw_lend_ETH", async function () { + await wallet0.sendTransaction({ + to: dsaWallet0.address, + value: ethers.utils.parseEther("10") + }); + const depositAmount = ethers.utils.parseEther("10"); + const fcash = ethers.utils.parseUnits("9", 8); + await helpers.depositAndLend(dsaWallet0, wallet0, wallet1, ETH_ID, true, depositAmount, MARKET_3M, fcash); + const before = await notional.getAccountPortfolio(dsaWallet0.address); + expect(before.length, "expect 1 lending position").to.be.equal(1); + expect(before[0][3], "expect 9 fETH").to.be.gte(ethers.utils.parseUnits("900000000", 0)); + await helpers.withdrawLend(dsaWallet0, wallet0, wallet1, ETH_ID, MARKET_3M, fcash); + const after = await notional.getAccountPortfolio(dsaWallet0.address); + expect(after.length, "expect lending position to be closed out").to.be.equal(0); + }); + }); + + describe("Borrow Tests", function () { + it("test_deposit_ETH_and_borrow_DAI_underlying", async function () { + await wallet0.sendTransaction({ + to: dsaWallet0.address, + value: ethers.utils.parseEther("10") + }); + const depositAmount = ethers.utils.parseEther("10"); + const fcash = ethers.utils.parseUnits("1000", 8); + await helpers.depositCollateralBorrowAndWithdraw( + dsaWallet0, wallet0, wallet1, ETH_ID, DEPOSIT_UNDERLYING, depositAmount, DAI_ID, MARKET_3M, fcash, true + ); + expect( + await daiToken.balanceOf(dsaWallet0.address), + "expect DSA wallet to contain borrowed balance minus fees" + ).to.be.gte(ethers.utils.parseEther("985")); + }); + + it("test_deposit_ETH_and_borrow_DAI_asset", async function () { + await wallet0.sendTransaction({ + to: dsaWallet0.address, + value: ethers.utils.parseEther("10") + }); + const depositAmount = ethers.utils.parseEther("10"); + const fcash = ethers.utils.parseUnits("1000", 8); + await helpers.depositCollateralBorrowAndWithdraw( + dsaWallet0, wallet0, wallet1, ETH_ID, DEPOSIT_UNDERLYING, depositAmount, DAI_ID, MARKET_3M, fcash, false + ); + expect( + await cdaiToken.balanceOf(dsaWallet0.address), + "expect DSA wallet to contain borrowed balance minus fees" + ).to.be.gte(ethers.utils.parseUnits("4490000000000", 0)); + }); + + it("test_deposit_DAI_underlying_and_borrow_ETH", async function () { + const depositAmount = ethers.utils.parseUnits("20000", 18); + await daiToken.connect(daiWhale).transfer(dsaWallet0.address, depositAmount); + const fcash = ethers.utils.parseUnits("1", 8); + await helpers.depositCollateralBorrowAndWithdraw( + dsaWallet0, wallet0, wallet1, DAI_ID, DEPOSIT_UNDERLYING, depositAmount, ETH_ID, MARKET_3M, fcash, true + ); + expect( + await ethers.provider.getBalance(dsaWallet0.address), + "expect DSA wallet to contain borrowed balance minus fees" + ).to.be.gte(ethers.utils.parseEther("0.99")); + }); + + it("test_deposit_DAI_asset_and_borrow_ETH", async function () { + const depositAmount = ethers.utils.parseUnits("1000000", 8); + await cdaiToken.connect(cdaiWhale).transfer(dsaWallet0.address, depositAmount); + const fcash = ethers.utils.parseUnits("1", 8); + await helpers.depositCollateralBorrowAndWithdraw( + dsaWallet0, wallet0, wallet1, DAI_ID, DEPOSIT_ASSET, depositAmount, ETH_ID, MARKET_3M, fcash, true + ); + expect( + await ethers.provider.getBalance(dsaWallet0.address), + "expect DSA wallet to contain borrowed balance minus fees" + ).to.be.gte(ethers.utils.parseEther("0.99")); + }); + + it("test_mint_nDAI_underlying_and_borrow_ETH", async function () { + const depositAmount = ethers.utils.parseUnits("20000", 18); + await daiToken.connect(daiWhale).transfer(dsaWallet0.address, depositAmount); + const fcash = ethers.utils.parseUnits("1", 8); + await helpers.depositCollateralBorrowAndWithdraw( + dsaWallet0, wallet0, wallet1, DAI_ID, DEPOSIT_UNDERLYING_MINT_NTOKEN, depositAmount, ETH_ID, MARKET_3M, fcash, true + ); + expect( + await ethers.provider.getBalance(dsaWallet0.address), + "expect DSA wallet to contain borrowed balance minus fees" + ).to.be.gte(ethers.utils.parseEther("0.99")); + }); + + it("test_mint_nDAI_asset_and_borrow_ETH", async function () { + const depositAmount = ethers.utils.parseUnits("1000000", 8); + await cdaiToken.connect(cdaiWhale).transfer(dsaWallet0.address, depositAmount); + const fcash = ethers.utils.parseUnits("1", 8); + await helpers.depositCollateralBorrowAndWithdraw( + dsaWallet0, wallet0, wallet1, DAI_ID, DEPOSIT_ASSET_MINT_NTOKEN, depositAmount, ETH_ID, MARKET_3M, fcash, true + ); + expect( + await ethers.provider.getBalance(dsaWallet0.address), + "expect DSA wallet to contain borrowed balance minus fees" + ).to.be.gte(ethers.utils.parseEther("0.99")); + }); + }); + + describe("Withdraw Tests", function () { + it("test_withdraw_ETH_underlying", async function () { + await wallet0.sendTransaction({ + to: dsaWallet0.address, + value: ethers.utils.parseEther("10") + }); + const depositAmount = ethers.utils.parseEther("1"); + await helpers.depositCollteral(dsaWallet0, wallet0, wallet1, 1, depositAmount, true); + await helpers.withdrawCollateral(dsaWallet0, wallet0, wallet1, 1, ethers.constants.MaxUint256, true); + expect( + await ethers.provider.getBalance(dsaWallet0.address), + "expect DSA wallet to contain underlying funds" + ).to.be.gte(ethers.utils.parseEther("10")); + }); + + it("test_withdraw_ETH_asset", async function () { + await wallet0.sendTransaction({ + to: dsaWallet0.address, + value: ethers.utils.parseEther("10") + }); + const depositAmount = ethers.utils.parseEther("1"); + await helpers.depositCollteral(dsaWallet0, wallet0, wallet1, ETH_ID, depositAmount, true); + await helpers.withdrawCollateral(dsaWallet0, wallet0, wallet1, ETH_ID, ethers.constants.MaxUint256, false); + expect( + await cethToken.balanceOf(dsaWallet0.address), + "expect DSA wallet to contain cToken funds" + ).to.be.gte(ethers.utils.parseUnits("4900000000", 0)); + }); + + it("test_redeem_DAI_raw", async function () { + const depositAmount = ethers.utils.parseUnits("1000", 8); + await cdaiToken.connect(cdaiWhale).transfer(dsaWallet0.address, depositAmount); + await helpers.depositAndMintNToken(dsaWallet0, wallet0, wallet1, DAI_ID, depositAmount, false); + await helpers.redeemNTokenRaw(dsaWallet0, wallet0, wallet1, DAI_ID, true, MaxUint96) + const bal = await notional.callStatic.getAccountBalance(DAI_ID, dsaWallet0.address); + expect(bal[0], "expect cDAI balance after redemption").to.be.gte(ethers.utils.parseUnits("99000000000", 0)); + expect(bal[1], "expect 0 nDAI").to.be.equal(ethers.utils.parseEther("0")); + }); + + it("test_redeem_DAI_and_withdraw_redeem", async function () { + const depositAmount = ethers.utils.parseUnits("1000", 8); + await cdaiToken.connect(cdaiWhale).transfer(dsaWallet0.address, depositAmount); + await helpers.depositAndMintNToken(dsaWallet0, wallet0, wallet1, DAI_ID, depositAmount, false); + await helpers.redeemNTokenAndWithdraw(dsaWallet0, wallet0, wallet1, DAI_ID, MaxUint96, ethers.constants.MaxUint256, true); + const bal = await notional.callStatic.getAccountBalance(DAI_ID, dsaWallet0.address); + expect(bal[0], "expect 0 cDAI balance").to.be.equal(ethers.utils.parseEther("0")); + expect(bal[1], "expect 0 nDAI balance").to.be.equal(ethers.utils.parseEther("0")); + }); + + it("test_redeem_DAI_and_withdraw_no_redeem", async function () { + const depositAmount = ethers.utils.parseUnits("1000", 8); + await cdaiToken.connect(cdaiWhale).transfer(dsaWallet0.address, depositAmount); + await helpers.depositAndMintNToken(dsaWallet0, wallet0, wallet1, DAI_ID, depositAmount, false); + expect(await cdaiToken.balanceOf(dsaWallet0.address)).to.be.equal(ethers.utils.parseEther("0")); + await helpers.redeemNTokenAndWithdraw(dsaWallet0, wallet0, wallet1, DAI_ID, MaxUint96, ethers.constants.MaxUint256, false); + const bal = await notional.callStatic.getAccountBalance(DAI_ID, dsaWallet0.address); + expect(bal[0], "expect 0 cDAI balance").to.be.equal(ethers.utils.parseEther("0")); + expect(bal[1], "expect 0 nDAI balance").to.be.equal(ethers.utils.parseEther("0")); + expect( + await cdaiToken.balanceOf(dsaWallet0.address), + "expect DSA wallet to contain cToken funds" + ).to.be.gte(ethers.utils.parseUnits("99000000000", 0)); + }); + + it("test_redeem_DAI_and_deleverage", async function () { + const depositAmount = ethers.utils.parseUnits("20000", 18); + await daiToken.connect(daiWhale).transfer(dsaWallet0.address, depositAmount); + const fcash = ethers.utils.parseUnits("1", 8); + await helpers.depositCollateralBorrowAndWithdraw( + dsaWallet0, wallet0, wallet1, DAI_ID, DEPOSIT_UNDERLYING, depositAmount, ETH_ID, MARKET_3M, fcash, true + ); + const bal = await ethers.provider.getBalance(dsaWallet0.address); + await helpers.depositAndMintNToken(dsaWallet0, wallet0, wallet1, ETH_ID, bal, true); + const before = await notional.getAccountPortfolio(dsaWallet0.address); + expect(before.length, "expect 1 fDAI debt position").to.be.equal(1); + expect(before[0][3], "expect fDAI debt position to equal borrow amount").to.be.lte(ethers.utils.parseUnits("-100000000", 0)); + const fcash2 = ethers.utils.parseUnits("0.98", 8); + await helpers.redeemNTokenAndDeleverage(dsaWallet0, wallet0, wallet1, ETH_ID, MaxUint96, MARKET_3M, fcash2); + const after = await notional.getAccountPortfolio(dsaWallet0.address); + expect(after.length, "expect 1 fDAI debt position after deleverage").to.be.equal(1); + expect(after[0][3], "expect fDAI debt balance to go down after deleverage").to.be.lte(ethers.utils.parseUnits("-2000000", 0)); + }); + }); + + describe("Staking Tests", function () { + it("test_stake_ETH", async function () { + const depositAmount = ethers.utils.parseEther("1"); + await wallet0.sendTransaction({ + to: dsaWallet0.address, + value: depositAmount + }); + expect(await snote.balanceOf(dsaWallet0.address), "expect 0 initial sNOTE balance").to.be.equal(0); + await helpers.mintSNoteFromETH(dsaWallet0, wallet0, wallet1, BigNumber.from(0), depositAmount, BigNumber.from(0)); + expect(await snote.balanceOf(dsaWallet0.address), "expect sNOTE balance to increase").to.be.gte(ethers.utils.parseEther("297")) + }); + + it("test_stake_WETH", async function () { + const depositAmount = ethers.utils.parseEther("1"); + await wethToken.connect(wethWhale).transfer(dsaWallet0.address, depositAmount); + expect(await snote.balanceOf(dsaWallet0.address), "expect 0 initial sNOTE balance").to.be.equal(0); + await helpers.mintSNoteFromWETH(dsaWallet0, wallet0, wallet1, BigNumber.from(0), depositAmount, BigNumber.from(0)); + expect(await snote.balanceOf(dsaWallet0.address), "expect sNOTE balance to increase").to.be.gte(ethers.utils.parseEther("297")) + }); + + it("test_stake_BPT", async function () { + const depositAmount = ethers.utils.parseEther("1"); + await wallet0.sendTransaction({ + to: bptWhale.address, + value: depositAmount + }); + await bptToken.connect(bptWhale).transfer(dsaWallet0.address, depositAmount); + expect(await snote.balanceOf(dsaWallet0.address), "expect 0 initial sNOTE balance").to.be.equal(0); + await helpers.mintSNoteFromBPT(dsaWallet0, wallet0, wallet1, depositAmount); + expect(await snote.balanceOf(dsaWallet0.address), "expect sNOTE balance to increase").to.be.eq(depositAmount) + }); + + it("test_unstake_success", async function () { + const depositAmount = ethers.utils.parseEther("1"); + await wallet0.sendTransaction({ + to: bptWhale.address, + value: depositAmount + }); + await bptToken.connect(bptWhale).transfer(dsaWallet0.address, depositAmount); + await helpers.mintSNoteFromBPT(dsaWallet0, wallet0, wallet1, depositAmount); + await helpers.startCoolDown(dsaWallet0, wallet0, wallet1); + // Skip ahead 16 days + await hre.network.provider.send("evm_increaseTime", [1382400]) + await hre.network.provider.send("evm_mine") + await helpers.redeemSNote( + dsaWallet0, + wallet0, + wallet1, + ethers.constants.MaxUint256, + BigNumber.from(0), + BigNumber.from(0), + true + ); + expect(await noteToken.balanceOf(dsaWallet0.address)).to.be.gte(ethers.utils.parseUnits("50000000000", 0)); + expect(await provider.getBalance(dsaWallet0.address)).to.be.gte(ethers.utils.parseUnits("32500000000000000", 0)) + }); + + it("test_unstable_failure", async function () { + const depositAmount = ethers.utils.parseEther("1"); + await wallet0.sendTransaction({ + to: bptWhale.address, + value: depositAmount + }); + await bptToken.connect(bptWhale).transfer(dsaWallet0.address, depositAmount); + await helpers.mintSNoteFromBPT(dsaWallet0, wallet0, wallet1, depositAmount); + await expect(helpers.redeemSNote( + dsaWallet0, + wallet0, + wallet1, + ethers.constants.MaxUint256, + BigNumber.from(0), + BigNumber.from(0), + true + )).to.be.revertedWith("Not in Redemption Window"); + }); + }); +});