Batch Trades

A flexible set of methods that enable almost any combination of lending, borrowing and providing liquidity on Notional.

fCash trades in Notional are done by calling eitherbatchBalanceAndTradeAction or batchLend in the BatchAction.sol contract. Both methods require specifying an array of encoded trade objects. The contract address to call to will be the Notional address.

When specifying multiple currencies to batchLend and batchBalanceAndTradeAction they MUST be ordered ascending by currency id. Collateral checks will always be deferred to the end of the transaction.

Batch Lend

batchLend is a simpler version of batchBalanceTradeAction that is more gas efficient for lending. Due to the nature of the Notional AMM, it is not possible to exactly calculate deposit amounts for lending until run time (asset cash exchange rates and time to maturity are constantly drifting). batchLend allows the user to specify a lending trade and then will calculate the required deposit amount at runtime (rather than specify up front like batchBalanceAndTradeAction) and then just pull the required amount of cash from the account's wallet.

If an account has a matured lending position (or an internal cash balance), this will be applied to the deposit required for lending BEFORE attempting to pull any cash from the account's wallet. Therefore, when rolling matured lending positions forward, a withdraw and deposit is not required.

struct BatchLend {
    uint16 currencyId;
    // True if the contract should try to transfer underlying tokens instead
    // of asset tokens
    bool depositUnderlying;
    // Array of tightly packed 32 byte objects that represent trades.
    bytes32[] trades;
}

// Method signature on Notional
function batchLend(address account, BatchLend[] calldata actions) external;

// Example function for encoding a BatchLend array
function encodeLendTrade(
    uint16 currencyId,
    uint8 marketIndex,
    uint88 fCashAmount,
    uint32 minImpliedRate,
    bool useUnderlying
) internal pure returns (BatchLend[] memory action) {
    action = new BatchLend[](1);
    action[0].currencyId = currencyId;
    action[0].depositUnderlying = useUnderlying;
    action[0].trades = new bytes32[](1);
    action[0].trades[0] = bytes32(
        (uint256(uint8(TradeActionType.Lend)) << 248) |
        (uint256(marketIndex) << 240) |
        (uint256(fCashAmount) << 152) |
        (uint256(minImpliedRate) << 120)
    );
}

Trades can be manually encoded using the code on this page below or they can be calculated via the Trade Helpers provided by Notional (specifically getfCashLendFromDeposit and getDepositFromfCashLend).

Batch Balance Trade Action

batchBalanceAndTradeAction is more powerful method than batchLend and allows the caller to combine many trading operations on Notional into a single transaction. The lifecycle of a batchBalanceTradeAction is always as follows:

  1. If the calling account has any matured positions, they will be settled to cash balances at the very beginning of the transaction.

  2. Deposit Actions are performed first and increase the cash available to the account to perform trades.

  3. Trades are performed and generate a net increase or decrease in the cash available to the account.

  4. Withdraws are performed and will return any specified amount of cash back to the user.

Deposit Action

These are the possible deposit actions to specify

  • None: no deposit will be performed.

  • DepositAsset: deposits asset cash (i.e. cDAI, cUSDC, aFRAX) into the account's cash balance. This will be used as collateral that earns variable interest based on Compound or Aave supply rates. depositActionAmount is denominated in the asset token's native precision.

  • DepositUnderlying: deposits underlying tokens (i.e. DAI, USDC, FRAX) and converts it into asset cash. This will be used as collateral that earns variable interest based on Compound or Aave supply rates. depositActionAmount is denominated in the underlying token's native precision.

  • DepositAssetAndMintNToken or DepositUnderlyingAndMintNToken: executes a deposit and then uses all of the cash to mint nTokens. nTokens earn the variable interest rate from asset cash, trading fees from Notional, and incentives in the form of NOTE. nTokens are also automatically considered collateral for borrowing.

  • RedeemNToken: redeems depositActionAmount of nTokens into an asset cash balance. This is considered a deposit action because it increases the cash available to the account.

  • ConvertCashToNToken: converts depositActionAmount worth of existing cash balance into nTokens. No transfer from the user's wallet is executed.

Trades

Each trade is a tightly packed bytes32 value. A single batch action may include multiple trades. Lend and borrow are the most common trade types. The easiest way to get encoded trade values is via the Trade Helper calculation views. Lend trades can be generated using getfCashLendFromDeposit and getDepositFromfCashLend, Borrow trades can be generated using getfCashBorrowFromPrincipal and getPrincipalFromfCashBorrow.

There are two other special types of trades which are not covered here and only used in special circumstances:

  • PurchaseNTokenResidual

  • SettleCashDebt

AddLiquidity and RemoveLiquidity are currently inactive. Use nTokens to provide liquidity to Notional.

Withdraw

  • withdrawAmountInternalPrecision: some amount of cash balance (denominated in Notional internal 8 decimal precision) to withdraw. It is not common to need to specify this value unless there is a specific need. The other two parameters are more common.

  • withdrawEntireCashBalance: withdraws an entire cash balance back to the account's wallet. Set this to true when lending to have any residuals from depositActionAmount returned to the wallet (or use batchLend instead to avoid this issue altogether) or set this to true to receive all the cash from executing a borrow trade.

  • redeemToUnderlying: set this to true for the account to receive the withdrawn cash as underlying tokens (i.e. DAI, USDC, ETH, etc).

Examples

This is an example of generating an encoded borrow trade:

struct BalanceActionWithTrades {
    DepositActionType actionType;
    uint16 currencyId;
    uint256 depositActionAmount;
    uint256 withdrawAmountInternalPrecision;
    bool withdrawEntireCashBalance;
    bool redeemToUnderlying;
    bytes32[] trades;
}

function batchBalanceAndTradeAction(
  address account,
  BalanceActionWithTrades[] calldata actions
) external payable;

// Example for encoding a BatchActionWithTrades object
function buildBalanceActionObject(
    uint16 currencyId,
    uint8 marketIndex,
    uint88 fCashAmount,
    uint32 maxImpliedRate,
    bool toUnderlying
) internal pure returns (BalanceActionWithTrades[] memory action) {
    action = new BalanceActionWithTrades[](1);
    action[0].actionType = DepositActionType.None;
    action[0].currencyId = currencyId;
    action[0].withdrawEntireCashBalance = true;
    action[0].redeemToUnderlying = toUnderlying;
    action[0].trades = new bytes32[](1);
    action[0].trades[0] = encodeBorrowTrade(
      marketIndex, fCashAmount, maxImpliedRate
    );
}

function encodeBorrowTrade(
    uint8 marketIndex,
    uint88 fCashAmount,
    uint32 maxImpliedRate
) internal pure returns (bytes32) {
   return bytes32(
        (uint256(uint8(TradeActionType.Borrow)) << 248) |
        (uint256(marketIndex) << 240) |
        (uint256(fCashAmount) << 152) |
        (uint256(maxImpliedRate) << 120)
    );
}

Deposit ETH and Borrow DAI

An example where the account deposits 1 ETH and borrows 100 DAI in a single transaction using the code above:

// Deposit 1 ETH as collateral, Borrow DAI
// REMEMBER: actions must be sorted by currency id in an ascending order
BatchBalanceAndTradeAction[] actions = new BatchBalanceAndTradeAction[](2)
actions[0] = BatchBalanceAndTradeAction({
  actionType: DepositUnderlying, // Deposit ETH, not cETH
  currencyId: 1, // ETH
  depositAmount: 1e18, // This is specified in eth's 18 decimal precision
  withdrawAmountInternalPrecision: 0,
  withdrawEntireCashBalance: false,
  redeemToUnderlying: false,
  // no trades, just depositing
})


actions[1] = BatchBalanceAndTradeAction({
  actionType: None,
  currencyId: 2, // DAI
  depositAmount: 0, 
  withdrawAmountInternalPrecision: 0,
  withdrawEntireCashBalance: true, // Will withdraw all the borrowed DAI
  redeemToUnderlying: true, // Converts to DAI from cDAI
  trades: new bytes32[](1)
})

action[1].trades[0] = encodeBorrowTrade(
  3, // Borrow from marketIndex 3, 1 year tenor
  100e8, // Sell 100 fCash, equivalent to paying 100e18 DAI at maturity
  0.05e9 // Do not borrow at a higher rate than 5% annualized    
);

// Submit trade, send 1 ETH for the deposit
NotionalProxy.batchBalanceAndTradeAction{value: 1e18}(account, actions);

Lend ETH and Borrow DAI

Notional also allows you to borrow against fCash assets for greater capital efficiency. In this example, instead of depositing ETH, the account will lend it for a fixed yield and then borrow DAI against it. The account's collateral is earning more interest than in the example above (in that example the ETH is converted to cETH and earn's Compound's lower variable interest). In this example, we use the Trade Helpers to calculate the appropriate values.

// Lend 1 ETH as collateral, Borrow DAI
// REMEMBER: actions must be sorted by currency id in an ascending order
MarketParameters[] memory ethMarkets = NotionalProxy.getActiveMarkets(1);
(
  /* fCashAmount */,
  /* marketIndex*/,
  bytes32 encodedLendETHTrade
) = NotionalProxy.getfCashLendFromDeposit({
  currencyId: 1, // ETH
  depositAmountExternal: 1e18,
  maturity: ethMarkets[0].maturity, // Lend to the first ETH market (3 month)
  minLendRate: 0, // Lend regardless of the interest rate
  blockTime: block.timestamp, // Calculate as of current time
  useUnderlying: true // Specifying ETH, not cETH
});

BatchBalanceAndTradeAction[] actions = new BatchBalanceAndTradeAction[](2);
actions[0] = BatchBalanceAndTradeAction({
  actionType: DepositUnderlying, // Deposit ETH, not cETH
  currencyId: 1, // ETH
  depositAmount: 1e18, // This is specified in eth's 18 decimal precision
  withdrawAmountInternalPrecision: 0,
  withdrawEntireCashBalance: false,
  redeemToUnderlying: false,
  trades: new bytes32[](1)
})
actions[0].trades[0] = encodedLendETHTrade;

actions[1] = BatchBalanceAndTradeAction({
  actionType: None,
  currencyId: 2, // DAI
  depositAmount: 0, 
  withdrawAmountInternalPrecision: 0,
  withdrawEntireCashBalance: true, // Will withdraw all the borrowed DAI
  redeemToUnderlying: true, // Converts to DAI from cDAI
  trades: new bytes32(1)
})

action[1].trades[0] = encodeBorrowTrade(
  3, // Borrow from marketIndex 3, 1 year tenor
  100e8, // Sell 100 fCash, equivalent to paying 100e18 DAI at maturity
  0.05e9 // Do not borrow at a higher rate than 5% annualized    
);

// Submit trade, send 1 ETH for the lending
NotionalProxy.batchBalanceAndTradeAction{value: 1e18}(account, actions);

Last updated