// SPDX-License-Identifier: GPL-3.0-only pragma solidity >0.7.0; pragma experimental ABIEncoderV2; import "./TradingAction.sol"; import "./nTokenMintAction.sol"; import "./nTokenRedeemAction.sol"; import "../SettleAssetsExternal.sol"; import "../FreeCollateralExternal.sol"; import "../../math/SafeInt256.sol"; import "../../global/StorageLayoutV1.sol"; import "../../internal/balances/BalanceHandler.sol"; import "../../internal/portfolio/PortfolioHandler.sol"; import "../../internal/AccountContextHandler.sol"; import "interfaces/notional/NotionalCallback.sol"; contract BatchAction is StorageLayoutV1 { using BalanceHandler for BalanceState; using PortfolioHandler for PortfolioState; using AccountContextHandler for AccountContext; using SafeInt256 for int256; /// @notice Executes a batch of balance transfers including minting and redeeming nTokens. /// @param account the account for the action /// @param actions array of balance actions to take, must be sorted by currency id /// @dev emit:CashBalanceChange for each balance /// @dev auth:msg.sender auth:ERC1155 function batchBalanceAction(address account, BalanceAction[] calldata actions) external payable { require(account == msg.sender || msg.sender == address(this), "Unauthorized"); // Return any settle amounts here to reduce the number of storage writes to balances ( AccountContext memory accountContext, SettleAmount[] memory settleAmounts ) = _settleAccountIfRequiredAndStorePortfolio(account); uint256 settleAmountIndex; BalanceState memory balanceState; for (uint256 i; i < actions.length; i++) { if (i > 0) { require(actions[i].currencyId > actions[i - 1].currencyId, "Unsorted actions"); } settleAmountIndex = _preTradeActions( account, settleAmountIndex, actions[i].currencyId, settleAmounts, balanceState, accountContext ); _executeDepositAction( account, balanceState, actions[i].actionType, actions[i].depositActionAmount ); _calculateWithdrawActionAndFinalize( account, accountContext, balanceState, actions[i].withdrawAmountInternalPrecision, actions[i].withdrawEntireCashBalance, actions[i].redeemToUnderlying ); } // Finalize remaining settle amounts BalanceHandler.finalizeSettleAmounts(account, accountContext, settleAmounts); _finalizeAccountContext(account, accountContext); } /// @notice Executes a batch of balance transfers and trading actions /// @param account the account for the action /// @param actions array of balance actions with trades to take, must be sorted by currency id /// @dev emit:CashBalanceChange for each balance, emit:BatchTradeExecution for each trade set, emit:nTokenSupplyChange /// @dev auth:msg.sender auth:ERC1155 function batchBalanceAndTradeAction(address account, BalanceActionWithTrades[] calldata actions) external payable { require(account == msg.sender || msg.sender == address(this), "Unauthorized"); AccountContext memory accountContext = _batchBalanceAndTradeAction(account, actions); _finalizeAccountContext(account, accountContext); } function batchBalanceAndTradeActionWithCallback( address account, BalanceActionWithTrades[] calldata actions, bytes calldata callbackData ) external payable { require(authorizedCallbackContract[msg.sender], "Unauthorized"); AccountContext memory accountContext = _batchBalanceAndTradeAction(account, actions); accountContext.setAccountContext(account); // Be sure to set the account context before initiating the callback NotionalCallback(msg.sender).notionalCallback(msg.sender, account, callbackData); if (accountContext.hasDebt != 0x00) { FreeCollateralExternal.checkFreeCollateralAndRevert(account); } } function _batchBalanceAndTradeAction( address account, BalanceActionWithTrades[] calldata actions ) internal returns (AccountContext memory) { ( AccountContext memory accountContext, SettleAmount[] memory settleAmounts, PortfolioState memory portfolioState ) = _settleAccountIfRequiredAndReturnPortfolio(account); uint256 settleAmountIndex; BalanceState memory balanceState; for (uint256 i; i < actions.length; i++) { if (i > 0) { require(actions[i].currencyId > actions[i - 1].currencyId, "Unsorted actions"); } settleAmountIndex = _preTradeActions( account, settleAmountIndex, actions[i].currencyId, settleAmounts, balanceState, accountContext ); _executeDepositAction( account, balanceState, actions[i].actionType, actions[i].depositActionAmount ); if (actions[i].trades.length > 0) { int256 netCash; if (accountContext.bitmapCurrencyId != 0) { require( accountContext.bitmapCurrencyId == actions[i].currencyId, "Invalid trades for account" ); bool didIncurDebt; (netCash, didIncurDebt) = TradingAction.executeTradesBitmapBatch( account, accountContext, actions[i].trades ); if (didIncurDebt) { accountContext.hasDebt = accountContext.hasDebt | Constants.HAS_ASSET_DEBT; } } else { // NOTE: we return portfolio state here instead of setting it inside executeTradesArrayBatch // because we want to only write to storage once after all trades are completed (portfolioState, netCash) = TradingAction.executeTradesArrayBatch( account, actions[i].currencyId, portfolioState, actions[i].trades ); } // If the account owes cash after trading, ensure that it has enough if (netCash < 0) _checkSufficientCash(balanceState, netCash.neg()); balanceState.netCashChange = balanceState.netCashChange.add(netCash); } _calculateWithdrawActionAndFinalize( account, accountContext, balanceState, actions[i].withdrawAmountInternalPrecision, actions[i].withdrawEntireCashBalance, actions[i].redeemToUnderlying ); } if (accountContext.bitmapCurrencyId == 0) { accountContext.storeAssetsAndUpdateContext(account, portfolioState, false); } // Finalize remaining settle amounts BalanceHandler.finalizeSettleAmounts(account, accountContext, settleAmounts); return accountContext; } /// @dev Loads balances, nets off settle amounts and then executes deposit actions function _preTradeActions( address account, uint256 settleAmountIndex, uint256 currencyId, SettleAmount[] memory settleAmounts, BalanceState memory balanceState, AccountContext memory accountContext ) private returns (uint256) { while ( settleAmountIndex < settleAmounts.length && settleAmounts[settleAmountIndex].currencyId < currencyId ) { // Loop through settleAmounts to find a matching currency settleAmountIndex += 1; } // This saves a number of memory allocations balanceState.loadBalanceState(account, currencyId, accountContext); if ( settleAmountIndex < settleAmounts.length && settleAmounts[settleAmountIndex].currencyId == currencyId ) { balanceState.netCashChange = settleAmounts[settleAmountIndex].netCashChange; // Set to zero so that we don't double count later settleAmounts[settleAmountIndex].netCashChange = 0; } return settleAmountIndex; } /// @dev Executes deposits function _executeDepositAction( address account, BalanceState memory balanceState, DepositActionType depositType, uint256 depositActionAmount_ ) private { int256 depositActionAmount = int256(depositActionAmount_); int256 assetInternalAmount; require(depositActionAmount >= 0); if (depositType == DepositActionType.None) { return; } else if ( depositType == DepositActionType.DepositAsset || depositType == DepositActionType.DepositAssetAndMintNToken ) { assetInternalAmount = balanceState.depositAssetToken( account, depositActionAmount, false // no force transfer ); } else if ( depositType == DepositActionType.DepositUnderlying || depositType == DepositActionType.DepositUnderlyingAndMintNToken ) { assetInternalAmount = balanceState.depositUnderlyingToken(account, depositActionAmount); } else if (depositType == DepositActionType.ConvertCashToNToken) { // _executeNTokenAction, will check if the account has sufficient cash assetInternalAmount = depositActionAmount; } _executeNTokenAction( account, balanceState, depositType, depositActionAmount, assetInternalAmount ); } /// @dev Executes nToken actions function _executeNTokenAction( address account, BalanceState memory balanceState, DepositActionType depositType, int256 depositActionAmount, int256 assetInternalAmount ) private { // After deposits have occurred, check if we are minting nTokens if ( depositType == DepositActionType.DepositAssetAndMintNToken || depositType == DepositActionType.DepositUnderlyingAndMintNToken || depositType == DepositActionType.ConvertCashToNToken ) { _checkSufficientCash(balanceState, assetInternalAmount); balanceState.netCashChange = balanceState.netCashChange.sub(assetInternalAmount); // Converts a given amount of cash (denominated in internal precision) into nTokens int256 tokensMinted = nTokenMintAction.nTokenMint( balanceState.currencyId, assetInternalAmount ); balanceState.netNTokenSupplyChange = balanceState.netNTokenSupplyChange.add( tokensMinted ); } else if (depositType == DepositActionType.RedeemNToken) { require( // prettier-ignore balanceState .storedNTokenBalance .add(balanceState.netNTokenTransfer) // transfers would not occur at this point .add(balanceState.netNTokenSupplyChange) >= depositActionAmount, "Insufficient token balance" ); balanceState.netNTokenSupplyChange = balanceState.netNTokenSupplyChange.sub( depositActionAmount ); int256 assetCash = nTokenRedeemAction(address(this)).nTokenRedeemViaBatch( balanceState.currencyId, depositActionAmount ); balanceState.netCashChange = balanceState.netCashChange.add(assetCash); } } /// @dev Calculations any withdraws and finalizes balances function _calculateWithdrawActionAndFinalize( address account, AccountContext memory accountContext, BalanceState memory balanceState, uint256 withdrawAmountInternalPrecision, bool withdrawEntireCashBalance, bool redeemToUnderlying ) private { int256 withdrawAmount = int256(withdrawAmountInternalPrecision); require(withdrawAmount >= 0); // dev: withdraw action overflow if (withdrawEntireCashBalance) { // This option is here so that accounts do not end up with dust after lending since we generally // cannot calculate exact cash amounts from the liquidity curve. withdrawAmount = balanceState.storedCashBalance.add(balanceState.netCashChange).add( balanceState.netAssetTransferInternalPrecision ); // If the account has a negative cash balance then cannot withdraw if (withdrawAmount < 0) withdrawAmount = 0; } // prettier-ignore balanceState.netAssetTransferInternalPrecision = balanceState .netAssetTransferInternalPrecision .sub(withdrawAmount); balanceState.finalize(account, accountContext, redeemToUnderlying); } function _finalizeAccountContext(address account, AccountContext memory accountContext) private { // At this point all balances, market states and portfolio states should be finalized. Just need to check free // collateral if required. accountContext.setAccountContext(account); if (accountContext.hasDebt != 0x00) { FreeCollateralExternal.checkFreeCollateralAndRevert(account); } } /// @notice When lending, adding liquidity or minting nTokens the account must have a sufficient cash balance /// to do so. function _checkSufficientCash(BalanceState memory balanceState, int256 amountInternalPrecision) private pure { require( amountInternalPrecision >= 0 && balanceState.storedCashBalance.add(balanceState.netCashChange).add( balanceState.netAssetTransferInternalPrecision ) >= amountInternalPrecision, "Insufficient cash" ); } function _settleAccountIfRequiredAndReturnPortfolio(address account) private returns ( AccountContext memory, SettleAmount[] memory, PortfolioState memory ) { AccountContext memory accountContext = AccountContextHandler.getAccountContext(account); if (accountContext.mustSettleAssets()) { return SettleAssetsExternal.settleAssetsAndReturnAll(account, accountContext); } return ( accountContext, new SettleAmount[](0), PortfolioHandler.buildPortfolioState(account, accountContext.assetArrayLength, 0) ); } function _settleAccountIfRequiredAndStorePortfolio(address account) private returns (AccountContext memory, SettleAmount[] memory) { AccountContext memory accountContext = AccountContextHandler.getAccountContext(account); SettleAmount[] memory settleAmounts; if (accountContext.mustSettleAssets()) { return SettleAssetsExternal.settleAssetsAndStorePortfolio(account, accountContext); } return (accountContext, settleAmounts); } }