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<>WETHsUSDe<>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
userTotalEquityto NAV - if 24 hours have elapsed, compute
delta_carry = carry_t - lastDailyCarry - accumulate shadow drawdown if
delta_carry < 0 - update
lastDailyCarryandlastDailyTimestamp - kill any position where
shadowDrawdown >= depositorequity < 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.