# Guide: Building Sense Adapters

This guide will unpack the Sense Core architecture, explain how to build a sense adapter in nine(!) steps, and guide builders in launching their own fixed-income application atop Sense’s infrastructure.

With Sense, builders can permissionlessly (opens new window) enable fixed rates in their ecosystem and/or experiment with novel fixed income mechanisms.

Learning objectives

  • Understand the role of Sense Adapters
  • Understand the various features supported by the Sense Divider
  • How to best design an Adapter for your application & ecosystem
  • How to onboard an Adapter onto the Sense Protocol

Pre-requisites


# Phase 1: Education on Sense


# Architecture

Sense v1 is composed of three distinct systems of smart contracts:

  1. Sense Core - Divide an asset with some arbitrary division logic
  2. Sense Space - Capital efficient, custom AMM for zero-coupon bond-like assets (e.g. Principal Tokens (PTs) (opens new window))
  3. Sense Fuse Pool - Lending facility for borrowing, lending, and collateralizing positions with Sense’s assets.

Over the next few sections, you’ll learn about the Sense Core, as it’s the foundation for Sense adapters.

# Sense Core

The Sense Core allows builders to divide any Target asset into two fixed-term IOUs, a Principal Token (PT) and a Yield Token (YT). The amount of value earmarked for PT holders and YT holders is defined by a scale function and is tailored to the builder’s application.

Later described in the Adapter Design section, the core deliberately pushes business logic into Adapters, giving them greater flexibility to explore the infinite design space of fixed income.

The core is composed of the following smart contracts: Divider, Adapters, Adapter Factories, and the Periphery. Contract source & their addresses are listed here. (opens new window)

Note: Although the token names represent the components of a yield-bearing vault (aka yield-bearing asset), the scale logic can accommodate any type of fixed income application.

# Divider

The Divider (opens new window) is the accounting engine of the Sense Core. It allows users to “divide” Target assets into ERC20 PTs and YTs with a particular maturity. All logic related to series management (initialization & settlement) and the PT/YT lifecycle (issue, combine, collect, and redeem) is housed within the Divider.

# Adapter & Adapter factories

Following a hub and spoke model, Adapters (opens new window) surround the Divider and hold logic related to their particular application, such as stripping yield from yield-bearing vaults. Once an Adapter is onboarded, users can initialize/settle series, issue PTs/YTs, and collect/redeem their Target via the Divider. The Adapter holds the Target before a series' maturity and contains logic to handle arbitrary airdrops from native or 3rd party liquidity mining programs.

There are two types of Adapters:

  1. Verified Adapters - verified by the Sense team and can be permissionlessly deployed by Adapter Factories
  2. Unverified Adapters - Any adapter that has not been individually verified by the Sense team or deployed by a verified Adapter factory. Assets from these adapters are not onboarded onto the Sense Fuse Pool.

The Adapter factory allows any person to deploy a Verified Adapter for a given Target in a permissionless manner. Most factories are similar except for how they implement _exists(), a method that checks whether the Target address supplied is a supported asset of that protocol.

# Periphery

The Periphery (opens new window) implements multiple-step functions, allowing every Sense user to benefit from atomic transactions. Initializing series, trading with Spaces, and adding/removing liquidity are all actions exposed by the Periphery and available to users of onboarded Adapters.

Other than series initialization, the current Periphery was designed to support Sense Space, so Adapters looking to leverage other trading / auction venues, such as Gnosis Batched auctions, will need to implement a new Periphery.


# Phase 2: Implementation

Now that we’re familiar with the Sense Core, let’s go through adapter designs for different fixed income applications. The three primary applications covered in this guide are:

  1. Yield-stripping - enables fixed rate lending in the underlying terms on some yield-bearing vault (e.g. fixed ETH yield on wstETH)
  2. Tranching - enables fixed rate lending in non-underlying terms on some yield-bearing vault (e.g. fixed DAI yield on the Uniswap V2 ETH-DAI LP share)
  3. DAO Bonds - allows DAOs to raise working capital without selling their native token

Before delving into Adapter design, we strongly recommend you make your yield-bearing vault compliant with the ERC4626 (opens new window) standard. With a compliant interface, your vaults can leverage existing Adapter implementations and be onboarded onto Sense with a single transaction. Moreover, ERC4626 is gaining wide adoption in the DeFi ecosystem (opens new window), so the benefits extend beyond the Sense integration.

Follow our development of the Yield-stripping Adapter (opens new window) for ERC4626 compliant Targets.

The guide’s design section starts with steps related to all Adapters, and then it splits off into steps tailored to each application.

# Step 1: Do you want a verified or unverified adapter?

Verified adapters have access to the Sense Fuse Pool (opens new window) and can be onboarded during the Guarded Launch. The Sense core team can verify your adapter after performing an informal security review.

After the guarded launch concludes, unverified adapters can be onboarded onto Sense without permission.

# Step 2: Are you building a Yield-stripping Adapter for an ERC4626 vault?

If yes → Pending completion of our reference Adapter, you’ll be able to deploy an Unverified / Verified Adapter for your vault by calling Periphery.deployAdapter(vault address, factory address). After you deploy your adapter, skip to Phase 3: Onboarding.

If no → read on.

# Step 3: Inherit the BaseAdapter or CropAdapter

We encourage all non-airdrop-receiving adapters to inherit the BaseAdapter for easy integration. It defines state variables & method interfaces inherent to the fixed income application.

If the Target is expecting to receive an airdrop during its lifetime, adapters should inherit the CropAdapter and override _claimReward, which allows the adapter to harvest airdrops from the Target and distribute them to YT holders.

# Step 4: Determine the State variables

The following state variables must be defined before the adapter is onboarded onto the Divider:

  1. target - address of the Target (e.g. cDAI, wstETH, Yearn Vault, etc)
  2. underlying - address of the Target’s underlying (e.g. DAI, WETH, etc)
  3. oracle - address to the Oracle of the Target's Underlying (e.g. Compound’s DAI oracle)
    1. Only relevant to Verified Adapters. Can be 0x for Unverified Adapters
  4. stake - token to stake at issuance. This contributes to the settlement reward for series settlers (e.g. ETH)
  5. stakeSize - amount to stake at issuance. This contributes to the settlement reward for series settlers (e.g. 0.25 ETH)
  6. minm - min time to maturity for a series (seconds after block.timestamp).
  7. maxm - max time to maturity for a series (seconds after block.timestamp).
  8. mode - maturity date type (0 for monthly, 1 for weekly). Do you want your series to mature at tops of months or weeks, 00:00 UTC?
  9. ifee - series issuance fee (opens new window). This fee goes to the series sponsor.
  10. tilt - any principal set aside for YTs at maturity (should be 0 for most adapters, other than those for Tranching applications).
  11. level - feature access codes. See below for more context.

Note on levels: Adapters can enable / disable the following features:

The number that level returns will be used to determine feature access by checking for binary digits using the following scheme: <onRedeem(y/n)><collect(y/n)><combine(y/n)><issue(y/n)>. (e.g. 0101 enables collect and issue, but not combine). What features are accessed are dependent on the adapter type. We’ll come back to this.

# Step 5: If your adapter is verified, override getUnderlyingPrice

Assets from verified adapters are onboarded onto and need to be priced by the Sense Fuse pool. getUnderlyingPrice should return the spot price of the underlying in ETH terms.

# Step 6: If your adapter will use Sense Space, override wrapUnderlying and unwrapTarget

The Periphery exposes utility functions that allows users to enter from / exit to the Underlying in atomic transactions. Adapters hold logic tailored to each protocol and abstract it away behind interfaces known by the Periphery.

The next sections describe how best to use scale, which decides how value is split up between PT & YT holders. The logic within scale is driven by the adapter, so let’s dive into each application. For advanced use of scale refer to Appendix - Step X: Understanding the dynamics of scale.


# Yield-stripping adapter

A yield-stripping adapter permits a user to decompose a yield-bearing vault into its principal and yield components and package them behind two fixed term, maturing assets, a Principal Token (PT) and a Yield Token (YT) (opens new window).

PTs redeem 1-for-1 for some yield-bearing vault equivalent to its underlying at maturity, whereas YTs deliver yield accrued on the underlying until maturity.

Check out our Use Cases (opens new window) docs to learn more about the utility of yield-stripping.

# Step 7: Override scale

Override & define scale for the particular yield-bearing vault (i.e. Target). Most Targets utilize the Rate Accumulator design (opens new window) pattern to record yield accrual through an onchain scaling factor.

Usually, scale can simply call the function that exposes the current Target scaling factor to achieve the following busines logic:

  • PTs redeem for 1-to-1 underlying worth of Target at maturity, and earn the variable Target interest rate after maturity.
  • YTs collect yield on the principal until maturity, and become worthless after maturity.

Examples: Compound cTokens (opens new window), Lido wstETH (opens new window)

# Step 8: Override scaleStored

Override & define scaleStored. This is just a view version of the Target’s scale. Most Targets expose a viewable scale by default, so we recommend just calling scale in scaleStored. Note, there are some special cases (e.g. Compound) where scale is not viewable by default, but these protocols typically expose a sister viewable scale function.

# Step 9: Define tilt and level in the Adapter

Set tilt to 0 and level to 31 (example in BaseFactory (opens new window)).


# Tranching adapter

WIP


# DAO Bonds adapter

WIP


# Phase 3: Onboarding

Once the adapter is written, tested, and audited, you’re ready to onboard it to the Sense Protocol.

# Step 1: Adapter Onboarding

During permissioned mode, builders can only onboard adapters by submitting a request to the Sense Core team, who has the privilege of onboarding the deployed adapter on the Divider.

Once you have written, tested and audited the adapter, you will need to submit the adapter by reaching out to the Sense team on the #dev channel in the Sense discord. The Sense Core team will perform some due diligence (review the code, check the provided audit report, check that the adapter is verified on etherscan, etc).

If due diligence returns positive, the Sense Core team will coordinate with you and proceed with verification. After Sense has enabled the permissionless mode (Divider.permissionless = true), you can can permissionlessly deploy adapters by calling Divider.addAdapter without needing verification from Sense.

# Step 2: Series Initialization

For all adapters, series are sponsored through Periphery.sponsorSeries(adapter, maturity, withPool) with the following parameters:

  • adapter - adapter address
  • maturity - maturity date for the series, in unix time. Must land on the top of the week/month, UTC00:00
  • withPool (only optional for unverified adapters) - whether to deploy a Space pool or not for the series’ assets

Series are initialized by Series Sponsors (opens new window), which are series actors, incentivized by issuance fees of the adapter. The adapter builder will likely need to be the first series sponsor, but it’s not a requirement, since Periphery.sponsorSeries is public.

# Step 3: Series Settlement

For all adapters, series are settled at or around maturity through Divider.settleSeries(adapter, maturity). As detailed here (opens new window), series sponsors have a grace period to settle the series, after which the settlement function is made public.

Defined by the scale function, PTs (& sometimes YTs) are redeemable for some portion of the Target at maturity.


# Summary

In this guide, we covered the Sense Core Architecture, encouraged adoption of ERC4626, and described the Adapter design space in the context of three fixed income applications, yield stripping, tranching, and DAO bonds.


# Additional Resources


# Need Help

Sense Discord - #dev channel


# Appendix


# Step X: Understanding the dynamics of scale

The Divider reads scale values (opens new window) from Adapters to determine how much Target to distribute to PT & YT holders before and at maturity. Moreover, if the Adapter defines a non-zero tilt, YT holders can redeem some Target after maturity, thereby creating “principal YTs”. As an academic exercise, the value distribution functions are as follows:

diagram

$$
\begin{equation*}
T_{PT}=\begin{cases}
          0 \quad &\text{if  } \, t < t_m \\

          \frac{(1-θ)}{s(t_m)} \quad &\text{if  } \, t \geq t_m
\cap \left(\frac{s(t_m)}{s_{max}(t_m)} \right)
\geq 1-θ \\

\frac{1}{s_{max}(t_m)} \quad &\text{if  } \, t \geq t_m
\cap \left(\frac{s(t_m)}{s_{max}(t_m)} \right) 
< 1-θ \\
     \end{cases}
\end{equation*}
$$

where,

T_pt = Amount of Target per PT

θ = tilt

s(t) = current scale at time t

s_max(t) = max scale seen until time t

t_m = maturity

t = current time

diagram

$$
\begin{equation*}
T_{YT}=\begin{cases}
          \left( \frac{1}{s(t_0)} - \frac{1}{s_m(t)}  \right) \quad &\text{if  } \, t < t_m   
\\

          \left( \frac{1}{s_{max}(t_m)} - \frac{1-θ}{s(t_m)}  \right) \quad &\text{if  } \, t \geq t_m
\cap \frac{s(t_m)}{s_{max}(t_m)} 
\geq 1-θ \\

0 \quad &\text{if  } \, t \geq t_m
\cap \frac{s(t_m)}{s_{max}(t_m)} 
< 1-θ \\
     \end{cases}
\end{equation*}
$$

where,

T_yt = Amount of Target per Collect YT

θ = tilt

s(t) = current scale at time t

s_max(t) = max scale seen until time t

t_0 = time of series initialization

The above functions are useful for advanced, esoteric use-cases, but they need not be mastered for builders of the applications described in this guide.

Last Updated: 12/8/2022, 8:16:29 PM