Wrapped fCash

A simplified compatibility wrapper for lending and holding fCash.

Wrapped fCash (wfCash) is a compatibility layer for developers who want to integrate with Notional lending but unable to use fCash's native ERC1155 specification. wfCash is compatible with ERC20 and ERC4626.

Technical Walkthrough: https://www.youtube.com/watch?v=RvCYFR2Yjls

GitHub: https://github.com/notional-finance/wrapped-fcash

Deployment

Each wfCash contract is deployed from the WrappedfCashFactory using a CREATE2 opcode and parameterized by a currency id and a maturity. Currency IDs are autoincrementing IDs generated by the Notional contract and correspond to a currency pair (i.e. USDC/cUSDC, DAI/cDAI, etc) that can be lent and borrowed on Notional.

The maturity is the unix timestamp of the block time when the fCash will mature.

One canonical wfCash contract can be deployed permissionlessly for every currencyId and maturity combination through the WrappedfCashFactory. Each wfCash contract is a OpenZeppelin BeaconProxy that refers to a single BeaconImplementation of the wfCashERC4626 contract.

The ERC4626 standard stipulates a single asset token for the contract. In this case we use the underlying token (i.e. DAI, USDC) rather than the money market token (i.e. cDAI, cUSDC). This choice was made in case the money market token changes in the future, the underlying token will not change in the future. As a consequence, ERC4626 interactions will be less gas efficient.

Example: Deploying an wfCash Wrapper

// Computes the wrapper address (will always be this address whether or
// not it has been deployed)
wrapperAddress = WrappedfCashFactory.computeAddress(2, 1664064000)

// Deploys a new fCash wrapper, wfDAI Maturing Sept 25, 2022 UTC Midnight
txn = WrappedfCashFactory.deployWrapper(2, 1664064000)

// These addresses will be the same
txn.events['WrapperDeployed']['wrapper'] == wrapperAddress

Lending (Minting wfCash)

Lending fCash can be done by calling mintViaAsset, mintViaUnderlying, mint or deposit. ERC4626 is provided as a compatibility layer but due to the computation required it is not the most gas efficient implementation. Using the non-ERC4626 mintViaAsset or mintViaUnderlying function is significantly more gas efficient and allows the user to apply slippage restrictions.

Creating wfCash tokens can also be done by using an ERC1155 safeTransferFrom to the corresponding wfCash contract from the main Notional contract. This allows accounts to seamlessly enter and exit wfCash positions from Notional ERC1155.

Notional ERC1155 fCash is automatically considered as collateral for borrowing. However, wfCash cannot be used in this way since it is held in escrow by the wfCash contract. This should not be a limitation for lend-only accounts.

Example: Minting Directly from wfCash

// Assuming that wrapper has been deployed:
IWrappedfCash wrapper = IWrappedfCash(
  WrappedfCashFactory.computeAddress(2, 1664064000)
);

// Approve the wrapper for DAI transfers
DAI.approve(address(wrapper), 100e18);

// Will purchase 100e8 fCash (100e18 DAI at maturity) and return any
// residual DAI back to the sender. Will not lend below 5% APY.
wrapper.mintViaUnderlying(100e18, 100e8, address(this), 0.05e9);

// Minting via ERC4626 (includes the fCash calculation), does the same as above
// but includes the deposit amount calculation and does not include any slippage
// protection. Sends the fCash lent to the `receiver` address.
wrapper.mint(100e8, receiver);

// Mints as much fCash as 100e18 DAI will purchase, if interest rates are 12%
// annualized then this would be approx 3% for a 3 month period, meaning this
// would purchase approx 103e8 fDAI. Sends the purchase fCash to the receiver.S
wrapper.deposit(100e18, recevier);

// Using cDAI
// Approve the wrapper for cDAI transfers
cDAI.approve(wrapper.address, 450e8); // Approx 100e18 DAI

// Will purchase 100e8 fCash (100e18 DAI at maturity) and return any
// residual cDAI back to the sender. Will not lend below 5% APY. Converts
// a variable lending position into a fixed lending position.
wrapper.mintViaUnderlying(4500e8, 100e8, address(this), 0.05e9);

Example: Mint via ERC1155 Transfer

This achieves the same as the method calls above but is approximately 20% more gas efficient if most of the computation is done off chain.

// Assuming that wrapper has been deployed:
IWrappedfCash wrapper = IWrappedfCash(
  WrappedfCashFactory.computeAddress(2, 1664064000)
);


(
  uint256 depositAmountUnderlying,
  /* */,
  /* */,
  bytes32 encodedTrade
) = getDepositFromfCashLend(
  2, // DAI
  100e8, // 100e18 fDAI at maturity
  1664064000, // same maturity as wrapper
  0.05e9, // No lending below 5% APY
  block.timestamp // calculate at current time
);

// Approve the Notional for DAI transfers
DAI.approve(address(notional), depositAmountUnderlying);

BatchLend[] memory action = new BatchLend(1);
action[0].currencyId = 2;
action[0].useUnderlying = true;
action[0].trades = new bytes32[](1);
action[0].trades[0] = encodedTrade;

// Encode the batch lending as calldata, Notional will execute this trade
// after exectuing a transfer between accounts.
bytes memory callData = abi.encodeWithSelector(
  NotionalProxy.batchLend.selector,
  address(this),
  action
);

// In this call, Notional will do the following:
// - Transfer 100e8 fDAI to the wrapper, minting 100e8 wrapped fDAI to
//   address(this)
// - address(this) temporarily incurs a -100e8 fDAI balance. If this is not
//   collateralized, the transaction will reverti.
// - address(this) lends 100e8 fDAI to net off their fDAI balance to exactly
//   zero using the batchLend encoded call.
// - address(this) now has no fDAI in Notional, and 100e8 fDAI in the wrapper
Notional.safeTransferFrom(
  address(this), // Transfer fCash from this address
  address(wrapper), // Send fCash to wrapper
  wrapper.fCashId(),  // Get the wrapper's fCash id
  100e8, // Send the 100e8 fDAI being purchased above
  callData // Execute the encoded trade on behalf of address(this)
);

Redemption

wfCash can be redeemed anytime before or after maturity. Redeeming fCash prior to maturity means selling it on Notional for cash (i.e. underlying or money market tokens). Selling fCash is subject to prevailing market interest rates and therefore does not guarantee that the holder will receive their promised fixed interest.

fCash (and wfCash is no different) settles to money market tokens (i.e. cTokens or aTokens) at maturity and will accrue the variable money market interest rate from that point forward. Accounts that redeem their wfCash after maturity will receive their fixed interest as well as any accrued variable rate interest on top.

Before or after maturity, redeeming wfCash can be done by calling redeem, redeemToUnderlying, redeemToAsset, or withdraw. Accounts are also able to call redeem and specify the transferfCash boolean in RedeemOpts to have the wfCash transfer the ERC1155 fCash token to their Notional account.

Do NOT expect that the withdraw method will return exactly the amount of assets specified. Due to rounding issues embedded within the contracts, there will be a dust amount of difference between the amount of assets supplied to the method and how much it actually withdraws to the user. Use balanceOf before and after the method call to get the exact amount of assets withdrawn. withdraw will transfer slightly less than the amount specified, to ensure that it always transfers slightly more increase the the amount specified by 10^(native token decimals - 4). (i.e. for 6 decimal USDC increase by 10^2 and for 10^18 DAI increase by 10^14).

redeem does not have these issues and can be used for more accurate accounting.

Example: Redeeming wfCash

// Assuming that wrapper has been deployed and the wallet has wfCash
IWrappedfCash wrapper = IWrappedfCash(
  WrappedfCashFactory.computeAddress(2, 1664064000)
);

// Before maturity, sells fCash to underlying tokens and sends the
// proceeds to address(this). Will not sell above 2% APY.
// After maturity, will withdraw 100e18 + some additional cDAI interest earned
// back to address(this), the maxImpliedRate slippage parameter is irreelvant
// after maturity.
wrapper.redeemToUnderlying(100e8, address(this), 0.02e9);

// Same as redeemToUnderlying, but sends proceeds to receiver. If
// msg.sender != address(this), will only pass if an ERC20 allowance has
// been set by address(this) for msg.sender.
wrapper.redeem(100e8, receiver, address(this));

// Will attempt to withdraw 100e18 DAI by selling fDAI prior to maturity. Will
// likely fail if address(this) has 100e8 fDAI because fDAI trades at a discount
// prior to maturity.
// WARNING: due to rounding and dust between 8 and 18 decimals, do NOT expect this
// method to return exactly 100e18 DAI in all cases. Use manual balanceOf checkin
// to get the exact DAI withdrawn
wrapper.withdraw(100e18, receiver, address(this));

Last updated