Skip to main content

The Carry Perp

The Carry Perp is a Carry Perpetual.

Cardinal bases it on a borrow loop spread: a user lends a starting asset in order to borrow a yield-bearing one, and loops that trade to multiply the difference between the yield and borrow rate.

The Carry Perp is synthetic. It settles against an LP pool doing the underlying spread at a larger size.

The product is compelling because the synthetic offering allows for more leverage than a natural construction of this trade allows for.

Markets

Cardinal will launch with two borrow loop spread markets:

  • wstETH<>WETH
  • sUSDe<>USDT

Position Value

Carry scales with leverage. Positive and negative carry rates impact margin at rates proportional to the leveraged yield.

Equity is updated every 12 seconds as carry moves in accord with Aave interest rates and the asset's native yield.

Fees

The Carry Perp has two fees.

Entry fee is paid when a position opens:

entry_fee = max(0, carry_at_entry) * notional / (365.25 * 24)

Performance fee is paid only on positive carry:

performance_fee = 35%

Fees route 90/10 LP/treasury.

Liquidation

Positions can be liquidated under two circumstances:

  • equity falls to 5% of initial deposit
  • shadow drawdown accumulates to the initial deposit size

Shadow drawdown is a monotonically increasing value, updated once per day regardless of tick frequency.

Every time carry goes relatively negative, positions accrue a drawdown. This is scaled by a protocol parameter, s_L, which will be set to 65 at launch.

Formula:

|delta_carry * notional / 365.25| / deposit * s_L

Example:

A user opens a position at 1000x leverage with a 1 ETH deposit when today's carry is +3%. If carry drops to 2.99%, the daily shadow-drawdown increment is:

abs(0.0001 * 1000 / 365.25) * 65 = 0.0178 ETH

Even if carry increases again tomorrow, their drawdown remains. Once drawdown accumulates to 1 ETH, their position is liquidatable.

Position Struct

Positions are tracked in one-contract-per-market deployments.

struct CarryPerpPosition {
uint id;
uint s_L; // protocol s_L at open, x100 for integer math
uint leverage; // selected tier, e.g. 1000
uint equity; // current position value, updated each 12s tick
uint shadowDrawdown; // accumulated shadow drawdown, updated daily
int lastDailyCarry; // carry_t as of last daily snapshot, for delta_carry computation
uint lastDailyTimestamp; // timestamp of last daily snapshot
uint deposit; // initial deposit
PositionStatusEnum status; // open, closed, killed
}

Methods

open(deposit, tier)

Checks that LPs have capacity to serve the requested position:

  • notional is less than 50% of globalNotionalCap
  • total notional across all open positions, including this new one, is less than globalNotionalCap

If OK, it transferFroms the user's tokens, deducts entry fee, initializes equity = deposit - entry_fee, and stores the position.

close(positionId)

User-initiated close. Pays out max(0, equity) to user and captures remainder to pool.

settleTick(carry_t, timestamp, positionIds[]) adminOnly

Called every 12 seconds.

For each position:

  • accrue signed leveraged carry to equity
  • subtract 35% performance fee if carry is positive
  • move positive carry from NAV to userTotalEquity
  • move negative carry from userTotalEquity to NAV
  • if 24 hours have elapsed, compute delta_carry = carry_t - lastDailyCarry
  • accumulate shadow drawdown if delta_carry < 0
  • update lastDailyCarry and lastDailyTimestamp
  • kill any position where shadowDrawdown >= deposit or equity < 5% * deposit
  • send remaining equity on kill to NAV
  • move Cardinal fees to treasuryAccrued

updateParams(tier, s_L) adminOnly

Governance lever.

treasurySweep

Anyone can call this. Transfers fees to the Cardinal treasury wallet.

Launch Values

sUSDe globalNotionalCap = $76.8M
wstETH globalNotionalCap = $322.6M

Pool NAV updates atomically inside settleTick. Kill captures are credited and fees route 90/10 LP/treasury.