Skip to content

How yield is measured

To decide where to put money, SAM needs to know how much each protocol is actually earning. It does not trust an admin feed or an external oracle. Instead it measures realized yield directly on-chain and annualizes it. The result is a per-protocol APR used only as a routing weight, it never affects how your shares are valued.

There are two parts to a protocol’s APR: the base (lending interest) and the reward (bonus token emissions). They are added together.

APRtotal=APRbase+APRreward\text{APR}_\text{total} = \text{APR}_\text{base} + \text{APR}_\text{reward}

APRs are in Fixed18, where 1e18 = 100%.

Base APR: from the protocol’s own price-per-share

Section titled “Base APR: from the protocol’s own price-per-share”

Lending protocols represent your deposit as receipt tokens (cTokens / market coins) whose per-share ratio rises as interest accrues. SAM records this ratio at each observation and measures how much it grew:

APRbase=(rnowrlast)MS_PER_YEAR1018rlastΔt\text{APR}_\text{base} = \frac{(r_\text{now} - r_\text{last}) \cdot \text{MS\_PER\_YEAR} \cdot 10^{18}} {r_\text{last} \cdot \Delta t}

where

  • r_now, r_last, the protocol’s per-share ratio now and at the previous observation,
  • Δt, milliseconds elapsed between them,
  • MS_PER_YEAR = 31_536_000_000 (the number of milliseconds in a year).

Reading it left to right: (r_now − r_last) / r_last is the fractional growth of the position over the window; dividing by Δt and multiplying by MS_PER_YEAR annualizes it; the 1e18 puts it in Fixed18.

Two guards keep this stable:

  • Observations closer together than 3 minutes (RATIO_MIN_ELAPSED_MS) are ignored, so a tiny window can’t produce a wild number.
  • If the ratio didn’t go up, the previous APR is kept rather than reading a negative or zero rate.

Reward APR: from realized, swapped rewards

Section titled “Reward APR: from realized, swapped rewards”

Some reserves pay separate reward tokens that never move the lending ratio. SAM harvests them, swaps them into the vault’s coin, and accumulates the realized value as a flow. That flow is then annualized against the deployed principal:

APRreward=flowMS_PER_YEAR1018PΔt\text{APR}_\text{reward} = \frac{\text{flow} \cdot \text{MS\_PER\_YEAR} \cdot 10^{18}}{P \cdot \Delta t}

where flow is the realized reward value (in the vault’s coin), P is the deployed principal, and Δt is the time since the last reward observation.

Guards:

  • The reward clock must span at least 1 hour (MIN_ELAPSED_MS) before a value is computed, so one lumpy harvest can’t masquerade as a permanent rate.
  • The result is clamped to 500% (MAX_REWARD_APR). A one-off harvest annualized over a short window can spike arbitrarily high; the clamp stops a transient spike from dominating routing.
  • A cycle that realizes no reward keeps the last learned value rather than dropping to zero, so the weight doesn’t flicker between harvests.

Every input is a value the protocol reports about your own position (its per-share ratio) or a reward SAM actually received and swapped. There is no external price feed to manipulate, and the APR only ever decides routing weights, it never changes your share price or how much you can withdraw. The worst a wrong weight could do is allocate sub-optimally; it cannot move funds anywhere they could be lost.