Dissecting the BarnBridge's SMART yield protocol

Tokenized Risk Protocol | Smart Bonds on Ethereum

This article is a collaboration with Andrew Hong. He has done an analysis on BarnBridge’s junior token holders’ PnL. Please visit his post after finishing this article!

Many DeFi users at the moment are degens, but as the DeFi ecosystem matures, there will be more participants who are risk-averse. They do not necessarily have to make three or four digits APR, but they do want a predictable return.

BarnBridge’s SMART (Structured Market Adjusted Risk Tranches) yield allows users to enter different tranches of combined debt pools from other DeFi protocols to normalize the risk curve by creating derivatives for risk mitigation. Users’ deposits are provided to the selected DeFi protocols and the yields generated are allocated back to users differently based on the tranches users are in.

There are two risk profiles in a BarnBridge pool. The senior tranche comes with lower risk as the yield rate is fixed. If the underlying money market is able to generate a higher return than the guaranteed rate, the extra yield goes to the junior tranche. The junior tranche comes with higher risk as the yield is variable. It can outperform or underperform the senior tranche depending on which way the market goes. Users in the junior tranche can even incur losses if the yield falls below the senior tranche’s guaranteed return, as they have to compensate the users in the senior tranche.

Let’s say in the Compound USDC pool, USDC deposited by investors in both tranches are pooled into Compound for lending and the pool receives cUSDC as well as COMP tokens in return. If the senior bond’s guaranteed yield is at 2%, and the yield generated by Compound is 2.5%, senior bond holders do not get the extra 0.5% as they will be redirected to junior token holders. Junior token holders will end up making 3% (2.5% from their own investment + 0.5% from the senior tranche). On the other hand, if Compound ends up generating only 1.5% yield, then the junior token holders’ yield will be only 1% as 0.5% will be taken out from their yield to make sure senior bond holders receive their guaranteed yield. So if the yield generated by Compound is less than 0.5%, the pool will even transfer junior token holders’ principals to senior bond holders.

We will use the Compound USDC pool as an example in this article.

Entering the junior tranche

When a user enters the junior tranche, jTokens (junior tokens), an ERC-20 token that represents ownership in the tranche, are minted at an 1:1 ratio to the underlying asset. The exchange rate between a jToken and its underlying asset represents the token holder’s gain/loss.

In order to buy junior tokens, SmartYield#buyTokens is called. It checks the following:

  • The purchase only goes through if the Compound controller does not pause the buy token function

  • The transaction cannot happen after the deadline

  • The number of jTokens minted cannot be less than minTokens

Then it does the following:

  • Calculate the amount of jTokens to mint (bb_cUSDC, bb stands for BarnBridge, and cUSDC is Compound’s yield bearing USDC)

  • Transfer the underlying asset to the Compound controller

  • Deposit the underlying asset to Compound

  • If there is a deposit fee, transfer the fee to the Compound controller’s fees owner

  • Updates the current state of Compound accumulation

  • Mint bb_cUSDC for the user

In order to calculate the price of each bb_cUSDC token, a few steps have to be taken.

  1. Calculate the underlying total balance, which is equal to the Compound pool’s balance minus the amount of USDC that have been liquidated through junior token liquidations.

  2. Calculate the gain amount paid to ABOND (Aggregate Bond).

    ABOND is an aggregate bond that represents all senior bonds minted. Its principal and gain are the sum of all senior bonds. Its issuance and maturity timestamp are the weighted average of all senior bonds. ABOND as an internal accounting tool is created to avoid computationally intensive tasks. For example, ABOND provides a reference point in the contract when determining a newly minted junior bond’s maturity, instead of looping through all senior bonds each time to find the weighted average maturity, thus saving on gas fees.

    The gain amount is equal to the final gain multiplied by the time elapsed within the paid duration.

  3. The underlying assets in the junior tranche is equal to the underlying total balance minus the ABOND’s principal and gain paid.

  4. The price is equal to the underlying assets in the junior tranche in proportion to the total supply of bb_cUSDC.

uint256 underlyingTotals = IProvider(pool).underlyingBalance().sub(underlyingLiquidatedJuniors);

uint256 duration = abond.maturesAt.sub(abond.issuedAt);
uint256 ts = block.timestamp * EXP_SCALE;
uint256 paidDuration = MathUtils.min(ts.sub(abond.issuedAt), duration);
uint256 abondPaid = abond.gain.mul(paidDuration).div(duration);

uint256 underlyingJuniors = underlyingTotals.sub(abond.principal).sub(abondPaid);

uint256 price = underlyingJuniors.mul(EXP_SCALE).div(totalSupply());

After depositing USDC into Compound, the Compound controller updates the current state of accumulation through the function updateCumulativesInternal. In this function, uniswapPriceCumulatives (used to estimate the amount of USDC that can be bought with the farmed COMP), cumulativeSupplyRate, cumulativeDistributionRate, prevCumulationTime, prevCompSupplyState and prevExchnageRateCurrent (there is a typo) are updated. It also adds a new observation that consists of yieldCumulative and timestamp to the yield oracle.

Entering the senior tranche

When a user enters the senior tranche, a NFT that represents the senior bond is minted. A bond carries principal, gain, issuance timestamp, maturity timestamp and liquidation control.

struct SeniorBond {
    // amount seniors put in
    uint256 principal;
    // amount yielded at the end. total = principal + gain
    uint256 gain;
    // bond was issued at timestamp
    uint256 issuedAt;
    // bond matures at timestamp
    uint256 maturesAt;
    // was it liquidated yet
    bool liquidated;
}

In order to buy a senior bond, SmartYield#buyBond is called. It checks the following:

  • The purchase only goes through if the Compound controller does not pause the buy bond function

  • The transaction cannot happen after the deadline

  • The bond’s lifespan cannot be greater than a bond’s maximum lifespan (default 90 days)

  • The gain has to be greater than the calculated bond gain through the specific bond model

  • The gain cannot be greater than the underlying loanable amount

To calculate the underlying loanable amount, we have to calculate the underlying total amount and the locked underlying amount. The locked underlying amount is equal to the ABOND’s principal and gain plus the value of tokens in junior bonds. The loanable amount is the difference between the underlying total amount and the locked underlying amount.

function underlyingLoanable() public virtual override returns (uint256) {
    // underlyingTotal - abond.principal - abond.gain - queued withdrawls
    uint256 _underlyingTotal = underlyingTotal();
    // abond.principal - abond.gain - (tokensInJuniorBonds * price() / EXP_SCALE)
    uint256 _lockedUnderlying = abond.principal.add(abond.gain).add(
      tokensInJuniorBonds.mul(price()).div(EXP_SCALE)
    );

    if (_lockedUnderlying > _underlyingTotal) {
      // abond.gain and (tokensInJuniorBonds in underlying) can overlap, so there is a cases where _lockedUnderlying > _underlyingTotal
      return 0;
    }

    // underlyingTotal() - abond.principal - abond.gain - (tokensInJuniorBonds * price() / EXP_SCALE)
    return _underlyingTotal.sub(_lockedUnderlying);
}

Then it does the following:

  • Transfer the underlying asset to the Compound controller

  • Deposit the underlying asset to Compound (no fee)

  • Updates the current state of Compound accumulation

  • Mint the senior bond for the user

Right before minting the senior bond, the contract accounts for the new bond and recalculates the ABOND’s new principal, gain, maturity timestamp and issuance timestamp.

  • The new principal and gain are just the sum of all senior bonds’ principals and gains

  • The new debt is the current debt plus the new bond’ gain

  • The new maturity timestamp is the weighted average of (current + new bond) maturity timestamp times debt

  • The new duration is the weighted average of (current + new bond) gain times the difference between new maturity timestamp and the current block timestamp

  • The new issuance timestamp is the new maturity timestamp minus the new duration

function _accountBond(SeniorBond memory b_) internal {
    uint256 _now = block.timestamp * EXP_SCALE;

    //abondDebt() + b_.gain
    uint256 newDebt = abondDebt().add(b_.gain);
    // for the very first bond or the first bond after abond maturity: abondDebt() = 0 => newMaturesAt = b.maturesAt
    // (abond.maturesAt * abondDebt() + b_.maturesAt * EXP_SCALE * b_.gain) / newDebt
    uint256 newMaturesAt = (abond.maturesAt.mul(abondDebt()).add(b_.maturesAt.mul(EXP_SCALE).mul(b_.gain))).div(newDebt);

    // (uint256(1) + ((abond.gain + b_.gain) * (newMaturesAt - _now)) / newDebt)
    uint256 newDuration = (abond.gain.add(b_.gain)).mul(newMaturesAt.sub(_now)).div(newDebt).add(1);
    // timestamp = timestamp - tokens * d / tokens
    uint256 newIssuedAt = newMaturesAt.sub(newDuration, "SY: liquidate some seniorBonds");

    abond = SeniorBond(
      abond.principal.add(b_.principal),
      abond.gain.add(b_.gain),
      newIssuedAt,
      newMaturesAt,
      false
    );
}

Instant withdrawal from the junior tranche

A bb_cUSDC holder has the option to sell his tokens before maturity, but he will have to forfeit his potential future gain in order to protect the senior bond holders’ guaranteed gains. This can be done by calling SmartYield#sellTokens.

The amount to forfeit is based on the bb_cUSDC’s holder’s decided amount to withdraw (tokenAmount_) in proportion to the total supply of the jToken pool.

// share of these tokens in the debt
// tokenAmount_ * EXP_SCALE / totalSupply()
uint256 debtShare = tokenAmount_.mul(EXP_SCALE).div(totalSupply());
// (abondDebt() * debtShare) / EXP_SCALE
uint256 forfeits = abondDebt().mul(debtShare).div(EXP_SCALE);
// debt share is forfeit, and only diff is returned to user
// (tokenAmount_ * price()) / EXP_SCALE - forfeits
uint256 toPay = tokenAmount_.mul(price()).div(EXP_SCALE).sub(forfeits);

Having the amount to return to the bb_cUSDC holder, the contract will

  • burn the bb_cUSDCs

  • redeem the underlying asset from Compound

    uint256 err = ICToken(cToken).redeemUnderlying(underlyingAmount_);
  • Updates the current state of Compound accumulation

  • Transfer the underlying asset (USDC) to the token holder

Withdrawal from the junior tranche through junior bonds

In order to not forfeit his gain, a bb_cUSDC holder can mint a junior bond using his jTokens, which he can only redeem on maturity. This can be done by calling SmartYield#buyJuniorBond.

The maturity timestamp has to be after the senior bonds’ weighted average maturity timestamp and it cannot be later than the maximum maturity timestamp set by the jToken holder.

uint256 maturesAt = abond.maturesAt.div(EXP_SCALE).add(1);

require(
  maturesAt <= maxMaturesAt_,
  "SY: buyJuniorBond maxMaturesAt"
);

bb_cUSDC is transferred to the contract and a junior bond is minted for the user.

JuniorBond memory jb = JuniorBond(
  tokenAmount_,
  maturesAt
);

address buyer = msg.sender;

_takeTokens(buyer, tokenAmount_);
_mintJuniorBond(buyer, jb);

As the junior bond is being minted, the contract has the account for the new junior bond. The contract has to

  • Update the number of jTokens in junior bonds

  • If there are no junior bonds maturing at the same time, the contracts adds the junior bond’s maturity timestamp to juniorBondsMaturities and sorts the array

  • Update the number of jTokens in junior bonds maturing at the time when the new bond matures (jBondsAt and jb_.maturesAt)

function _accountJuniorBond(JuniorBond memory jb_) internal {
    // tokensInJuniorBonds += jb_.tokens
    tokensInJuniorBonds = tokensInJuniorBonds.add(jb_.tokens);

    JuniorBondsAt storage jBondsAt = juniorBondsMaturingAt[jb_.maturesAt];
    uint256 tmp;

    if (jBondsAt.tokens == 0 && block.timestamp < jb_.maturesAt) {
      juniorBondsMaturities.push(jb_.maturesAt);
      for (uint256 i = juniorBondsMaturities.length - 1; i >= MathUtils.max(1, juniorBondsMaturitiesPrev); i--) {
        if (juniorBondsMaturities[i] > juniorBondsMaturities[i - 1]) {
          break;
        }
        tmp = juniorBondsMaturities[i - 1];
        juniorBondsMaturities[i - 1] = juniorBondsMaturities[i];
        juniorBondsMaturities[i] = tmp;
      }
    }

    // jBondsAt.tokens += jb_.tokens
    jBondsAt.tokens = jBondsAt.tokens.add(jb_.tokens);
}

In the case where the jToken holder mints the junior bond after the senior bonds have matured, the liquidation process is kicked off immediately.

The liquidation process consists of the following steps:

  1. Check if the junior bond’s price has been calculated. If not, calculate its price (see the example above on how to calculate bb_cUSDC’s price, it uses the same formula)

  2. Burn the jTokens and remove them from tokensInJuniorBonds and jBondsAt

  3. Add the bond value to underlyingLiquidatedJuniors and deduct the payment amount from it. This affects the calculation of underlying total asset

  4. Burn the bond

  5. Withdraw the underlying asset from Compound

  6. Send the underlying asset to the jToken holder

On the other hand, if the junior bond is minted before the senior bonds have matured, it will not be immediately redeemed. An attempt to redeem junior bonds will be triggered every time when any smart contract transaction is submitted through the function _beforeProviderOp. It loops through all junior bonds’ maturity timestamps and liquidates the ones who have reached maturity.

function _beforeProviderOp(uint256 upUntilTimestamp_) internal {
  // this modifier will be added to the begginging of all (write) functions.
  // The first tx after a queued liquidation's timestamp will trigger the liquidation
  // reducing the jToken supply, and setting aside owed_dai for withdrawals
  for (uint256 i = juniorBondsMaturitiesPrev; i < juniorBondsMaturities.length; i++) {
      if (upUntilTimestamp_ >= juniorBondsMaturities[i]) {
          _liquidateJuniorsAt(juniorBondsMaturities[i]);
          juniorBondsMaturitiesPrev = i.add(1);
      } else {
          break;
      }
  }
}

Withdrawing from the senior tranche

A senior bond holder can redeem his bond by calling SmartYield#redeemBond. If the bond has reached maturity, the function sends the bond holder principal + gain - fee.

require(
    block.timestamp >= seniorBonds[bondId_].maturesAt,
    "SY: redeemBond not matured"
);

// bondToken.ownerOf will revert for burned tokens
address payTo = IBond(seniorBond).ownerOf(bondId_);
// seniorBonds[bondId_].gain + seniorBonds[bondId_].principal
uint256 payAmnt = seniorBonds[bondId_].gain.add(seniorBonds[bondId_].principal);
uint256 fee = MathUtils.fractionOf(seniorBonds[bondId_].gain, IController(controller).FEE_REDEEM_SENIOR_BOND());
payAmnt = payAmnt.sub(fee);

The bond will be unaccounted for (ABOND attributes recalculation) and burned.

if (seniorBonds[bondId_].liquidated == false) {
    seniorBonds[bondId_].liquidated = true;
    _unaccountBond(seniorBonds[bondId_]);
}
                                                        
// bondToken.burn will revert for already burned tokens
IBond(seniorBond).burn(bondId_);

Similar to withdrawing a junior bond, the underlying asset will be withdrawn from Compound and sent to the bond holder.

Selling farmed tokens into underlying assets

Anyone can call CompoundController#harvest to sell the protocol’s farmed COMP tokens into the underlying asset (USDC). The caller will be rewarded a fraction of the COMP rewarded in USDC.

Before selling COMP for USDC, COMP has to be claimed from Compound.

ICToken cToken = ICToken(CompoundProvider(pool).cToken());
IERC20 uToken = IERC20(CompoundProvider(pool).uToken());
IComptroller comptroller = IComptroller(cToken.comptroller());
IERC20 rewardToken = IERC20(comptroller.getCompAddress());

// claim pool comp
address[] memory holders = new address[](1);
holders[0] = pool;
address[] memory markets = new address[](1);
markets[0] = address(cToken);
comptroller.claimComp(holders, markets, false, true);

The COMP is then transferred from the pool to the controller so that it can be sold.

rewardToken.safeTransferFrom(pool, address(this), rewardToken.balanceOf(pool));
uint256 compRewardTotal = rewardToken.balanceOf(address(this)); // COMP

Then the COMP is sold for USDC on Uniswap.

// pool share is (comp to underlying) - (harvest cost percent)
uint256 poolShare = MathUtils.fractionOf(
  quoteSpotCompToUnderlying(compRewardSold),
  EXP_SCALE.sub(HARVEST_COST)
);

// make sure we get at least the poolShare
IUniswapV2Router(uniswapRouter()).swapExactTokensForTokens(
    compRewardSold,
    poolShare,
    uniswapPath,
    address(this),
    block.timestamp
);

The USDC is deposited into the pool as yield for the jToken and bond holders.

CompoundProvider(pool)._takeUnderlying(address(this), poolShare);
CompoundProvider(pool)._depositProvider(poolShare, 0);

Lastly don’t forget the compensate the caller as he had to pay for gas fees!

uint256 callerReward = uToken.balanceOf(address(this));
uToken.safeTransfer(caller, callerReward);

Closing thoughts

BarnBridge expands the DeFi ecosystem by allowing risk-averse investors to participate through fixed-income bonds. The use of NFTs as bonds also shows us there are many use cases for NFTs beyond what the general public considers as bubbles (for example, digital art).