1. stETH vs wstETH Mechanics
Understanding the distinction between stETH and wstETH is fundamental before building any yield strategy. They represent the same underlying position but with very different accounting models that affect smart contract compatibility and gas efficiency.
stETH: Rebasing Token
stETH is a rebasing ERC-20 token. Every 24 hours (at 12:00 UTC), when Ethereum validators earn consensus and execution layer rewards, stETH balances in all wallets automatically increase. If you hold 10 stETH and the daily rebase adds 0.01%, you will hold 10.001 stETH the next day without any transaction.
This means stETH balances in smart contracts that don't explicitly handle rebasing will silently accumulate tokens. Many DeFi protocols cannot accommodate this. Agents using stETH directly in automated strategies must account for balance drift between rebase events.
wstETH: Wrapped Non-Rebasing Token
wstETH wraps stETH into a non-rebasing format. Instead of balance increasing, the exchange rate (wstETH per ETH) increases over time. This makes it compatible with all DeFi protocols and much safer for agents to track positions programmatically.
from web3 import Web3
WSTETH_ADDRESS = "0x7f39C581F595B53c5cb19bD0b3f8dA6c935E2Ca0"
STETH_ADDRESS = "0xae7ab96520DE3A18E5e111B5EaAb095312D7fE84"
WSTETH_ABI = [
{"name": "wrap", "type": "function", "inputs": [{"type": "uint256"}], "outputs": [{"type": "uint256"}]},
{"name": "unwrap", "type": "function", "inputs": [{"type": "uint256"}], "outputs": [{"type": "uint256"}]},
{"name": "stEthPerToken", "type": "function", "inputs": [], "outputs": [{"type": "uint256"}]},
{"name": "tokensPerStEth", "type": "function", "inputs": [], "outputs": [{"type": "uint256"}]},
]
def get_wsteth_exchange_rate(w3: Web3) -> float:
"""Get current stETH per wstETH (increases over time)."""
wsteth = w3.eth.contract(address=WSTETH_ADDRESS, abi=WSTETH_ABI)
rate = wsteth.functions.stEthPerToken().call()
return rate / 10**18 # e.g., 1.1234 stETH per wstETH
def wrap_steth(w3: Web3, account, steth_amount: int) -> str:
"""Convert stETH to wstETH for DeFi protocol use."""
wsteth = w3.eth.contract(address=WSTETH_ADDRESS, abi=WSTETH_ABI)
tx = wsteth.functions.wrap(steth_amount).build_transaction({
'from': account.address,
'gas': 120000,
'nonce': w3.eth.get_transaction_count(account.address)
})
signed = account.sign_transaction(tx)
tx_hash = w3.eth.send_raw_transaction(signed.rawTransaction)
return tx_hash.hex()
When to Use Each
| Use Case | Best Token | Reason |
|---|---|---|
| Aave v3 collateral | wstETH | Native support, no rebase issues |
| Curve stETH/ETH pool | stETH | Pool is built around rebasing stETH |
| MakerDAO vault | wstETH | stETH-B vault accepts wstETH-via-proxy |
| EigenLayer restaking | Both | Native ETH restaking or wstETH LST restaking |
| Pendle yield splitting | wstETH | Pendle wraps to PT/YT, wstETH preferred |
| Holding for staking yield | stETH | Direct daily rebase; no wrap gas cost |
2. Aave/Compound stETH Collateral
Using wstETH as collateral on Aave v3 allows agents to borrow USDC, DAI, or other assets against their staking position. The borrowed assets can then be deployed at yields that exceed the Aave borrow rate, creating a leveraged staking carry trade.
Aave wstETH Carry Trade Example
from web3 import Web3
AAVE_V3_POOL = "0x87870Bca3F3fD6335C3F4ce8392D69350B4fA4E2"
WSTETH = "0x7f39C581F595B53c5cb19bD0b3f8dA6c935E2Ca0"
USDC = "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"
class AaveLidoStrategy:
"""Leveraged stETH strategy via Aave v3."""
def __init__(self, w3: Web3, account):
self.w3 = w3
self.account = account
self.pool = w3.eth.contract(address=AAVE_V3_POOL, abi=AAVE_POOL_ABI)
def supply_wsteth(self, amount: int) -> str:
"""Supply wstETH as collateral to Aave v3."""
tx = self.pool.functions.supply(
WSTETH, amount, self.account.address, 0
).build_transaction({
'from': self.account.address,
'gas': 280000,
'nonce': self.w3.eth.get_transaction_count(self.account.address)
})
return self._sign_and_send(tx)
def borrow_usdc(self, amount: int, rate_mode: int = 2) -> str:
"""Borrow USDC against wstETH collateral (rate_mode=2 variable)."""
tx = self.pool.functions.borrow(
USDC, amount, rate_mode, 0, self.account.address
).build_transaction({
'from': self.account.address,
'gas': 250000,
'nonce': self.w3.eth.get_transaction_count(self.account.address)
})
return self._sign_and_send(tx)
async def get_health_factor(self) -> float:
"""Get position health factor (>1.0 = safe)."""
data = self.pool.functions.getUserAccountData(
self.account.address
).call()
# Returns: totalCollateralBase, totalDebtBase, availableBorrowsBase,
# currentLiquidationThreshold, ltv, healthFactor
health_factor = data[5] / 10**18
return health_factor
3. Curve stETH/ETH Pool LP
The Curve stETH/ETH pool is one of the deepest liquidity pools in DeFi and a primary venue for stETH yield stacking. LPs earn trading fees from ETH/stETH conversions plus CRV emissions from the gauge.
LP Mechanics
The pool maintains a near-1:1 balance between ETH and stETH. When LPs provide both assets, they earn:
- 0.04% swap fee on all ETH↔stETH trades
- CRV gauge emissions (boosted by veCRV voting)
- Lido's stETH rebase (the pool's stETH balance increases daily)
from web3 import Web3
CURVE_STETH_POOL = "0xDC24316b9AE028F1497c275EB9192a3Ea0f67022"
CURVE_STETH_LP = "0x06325440D014e39736583c165C2963BA99fAf14E"
CURVE_STETH_GAUGE = "0x182B723a58739a9c974cFDB385ceaDb237453c28"
class CurveStEthLP:
"""Manage Curve stETH/ETH LP position."""
def __init__(self, w3: Web3, account):
self.w3 = w3
self.account = account
self.pool = w3.eth.contract(address=CURVE_STETH_POOL, abi=CURVE_POOL_ABI)
self.gauge = w3.eth.contract(address=CURVE_STETH_GAUGE, abi=GAUGE_ABI)
def add_liquidity(
self,
eth_amount: int,
steth_amount: int,
min_lp_out: int
) -> str:
"""Add ETH + stETH to Curve pool and receive LP tokens."""
tx = self.pool.functions.add_liquidity(
[eth_amount, steth_amount], min_lp_out
).build_transaction({
'from': self.account.address,
'value': eth_amount,
'gas': 350000,
'nonce': self.w3.eth.get_transaction_count(self.account.address)
})
return self._sign_and_send(tx)
def stake_lp_in_gauge(self, lp_amount: int) -> str:
"""Stake LP tokens in gauge to earn CRV emissions."""
tx = self.gauge.functions.deposit(lp_amount).build_transaction({
'from': self.account.address,
'gas': 180000,
'nonce': self.w3.eth.get_transaction_count(self.account.address)
})
return self._sign_and_send(tx)
def get_claimable_crv(self) -> int:
"""Get unclaimed CRV balance."""
return self.gauge.functions.claimable_tokens(
self.account.address
).call()
def get_virtual_price(self) -> float:
"""Get LP token virtual price (increases with fees)."""
price = self.pool.functions.get_virtual_price().call()
return price / 10**18
Note: Consider depositing LP tokens into Convex Finance (cvxCRV) to receive boosted CRV + CVX rewards without needing to lock veCRV yourself. Convex typically boosts CRV yields by 2.5x for protocols with significant veCRV holdings.
4. EigenLayer Restaking for Additional Yield
EigenLayer allows stETH and wstETH holders to "restake" their liquid staking tokens (LSTs) to provide cryptoeconomic security to additional protocols called Actively Validated Services (AVSs). Restakers earn additional yield on top of base Lido staking rewards.
LST Restaking via EigenLayer
Agents deposit wstETH into an EigenLayer StrategyManager contract to receive restaking points. These points eventually convert to real yield from AVS fees once AVS mainnet launches complete.
EIGENLAYER_STRATEGY_MANAGER = "0x858646372CC42E1A627fcE94aa7A7033e7CF075A"
WSTETH_STRATEGY = "0x7CA911E83dabf90C90dD3De5411a10F1A6112184"
class EigenLayerRestaker:
"""Deposit wstETH into EigenLayer for restaking yield."""
def __init__(self, w3: Web3, account):
self.w3 = w3
self.account = account
self.strategy_mgr = w3.eth.contract(
address=EIGENLAYER_STRATEGY_MANAGER,
abi=STRATEGY_MANAGER_ABI
)
def deposit_into_strategy(self, token: str, strategy: str, amount: int) -> str:
"""Deposit LST token into EigenLayer strategy."""
tx = self.strategy_mgr.functions.depositIntoStrategy(
strategy, token, amount
).build_transaction({
'from': self.account.address,
'gas': 220000,
'nonce': self.w3.eth.get_transaction_count(self.account.address)
})
return self._sign_and_send(tx)
def get_shares(self, strategy: str) -> int:
"""Get agent's share balance in a given strategy."""
return self.strategy_mgr.functions.stakerStrategyShares(
self.account.address, strategy
).call()
def queue_withdrawal(
self,
strategies: list,
tokens: list,
shares: list,
withdrawer: str,
nonce: int
) -> str:
"""Queue withdrawal from EigenLayer (7-day delay)."""
tx = self.strategy_mgr.functions.queueWithdrawal(
strategies, tokens, shares, withdrawer, True
).build_transaction({
'from': self.account.address,
'gas': 300000,
'nonce': nonce
})
return self._sign_and_send(tx)
Liquidity Warning: EigenLayer restaked positions are subject to a 7-day withdrawal delay. Agents must account for this illiquidity period in their strategy design — do not restake assets that may be needed for quick liquidation defense in other positions.
5. Pendle stETH Yield Splitting
Pendle Finance allows agents to split yield-bearing assets like wstETH into two components:
- PT (Principal Token): Redeems at face value (1 wstETH) at maturity. Trades at a discount — effectively a fixed-rate bond.
- YT (Yield Token): Captures all future yield until maturity. Highly leveraged exposure to staking APR changes.
Agent Strategies with Pendle
Fixed-rate strategy: Buy PT-wstETH at a discount and hold to maturity. If 1 wstETH PT trades at 0.94 wstETH equivalent with 6 months to maturity, that's ~12% annualized fixed yield — regardless of what Lido's APR does during that period.
Yield speculation strategy: Buy YT-wstETH when you believe Ethereum staking rewards will increase (e.g., during high MEV periods or network upgrades). YT is leveraged long on yield.
LP strategy: Provide liquidity to Pendle AMMs to earn swap fees from traders splitting and combining PT/YT. Generally yields 8-15% APY including Pendle emissions.
class PendleStrategyAgent:
"""Pendle Finance integration for stETH yield optimization."""
def __init__(self, w3: Web3, account, market_address: str):
self.w3 = w3
self.account = account
self.market = w3.eth.contract(
address=market_address, abi=PENDLE_MARKET_ABI
)
self.router = w3.eth.contract(
address="0x00000000005BBB0EF59571E58418F9a4357b68A0",
abi=PENDLE_ROUTER_ABI
)
def get_pt_implied_apy(self) -> float:
"""Calculate implied fixed APY from PT current price."""
state = self.market.functions.readState(True).call()
last_ln_implied_rate = state[4]
implied_rate = (last_ln_implied_rate / 10**18)
import math
return (math.exp(implied_rate) - 1) * 100
def should_buy_pt(
self,
implied_apy: float,
current_steth_apy: float,
yield_outlook: str = "neutral"
) -> bool:
"""Determine if PT purchase makes sense."""
spread = implied_apy - current_steth_apy
if yield_outlook == "bearish": # expect yields to fall
return spread > 0.5 # any premium to current yield is good
elif yield_outlook == "bullish":
return spread > 3.0 # need large spread to justify locking
return spread > 1.5 # neutral: moderate spread threshold
6. stETH/ETH Depeg Monitoring
stETH has historically traded at a small discount to ETH (0.1-0.5% normally), with severe depegs during the June 2022 crisis (stETH fell to 0.933 ETH). Agents that monitor depeg events can either arbitrage the spread or defensively reduce stETH exposure before cascading liquidations.
import asyncio
from datetime import datetime
class StEthDepegMonitor:
"""Monitor stETH/ETH peg and alert on significant deviations."""
def __init__(self, curve_pool: Web3.eth.contract, alert_threshold: float = 0.005):
self.pool = curve_pool
self.alert_threshold = alert_threshold # 0.5% depeg
self.depeg_history = []
def get_steth_price(self) -> float:
"""Get stETH price in ETH from Curve pool."""
# dy for 1 stETH → ETH
eth_out = self.pool.functions.get_dy(1, 0, 10**18).call()
return eth_out / 10**18
async def monitor(self, callback, poll_interval: int = 60):
"""Continuously monitor depeg, call callback on alert."""
while True:
price = self.get_steth_price()
depeg = 1.0 - price
timestamp = datetime.utcnow().isoformat()
self.depeg_history.append({'time': timestamp, 'price': price, 'depeg': depeg})
if len(self.depeg_history) > 1440: # keep 24h of 1m samples
self.depeg_history.pop(0)
if depeg > self.alert_threshold:
await callback({
'type': 'DEPEG_ALERT',
'price': price,
'depeg_pct': depeg * 100,
'severity': 'HIGH' if depeg > 0.02 else 'MEDIUM'
})
await asyncio.sleep(poll_interval)
7. Slashing Risk Assessment
Lido validators are subject to slashing if they double-sign blocks or go offline for extended periods. While Lido has not experienced a significant slashing event to date, agents must quantify this tail risk in their position sizing models.
Key slashing risk parameters agents should monitor:
- Lido Node Operator set: 30+ vetted professional operators diversifies slashing exposure
- Insurance fund: Lido maintains a cover fund to compensate minor slashing events
- Correlation risk: A correlated bug affecting multiple operators simultaneously is the primary tail risk
- DVT adoption: Distributed Validator Technology (SSV, Obol) reduces single-operator risk as Lido adopts it
Risk Sizing: For conservative agents, cap stETH allocation at 40% of total portfolio ETH. For aggressive strategies (e.g., multiple Pendle YT positions), use smaller tranches and maintain escape velocity to exit positions within 24 hours.
8. Python LidoAgent
The following LidoAgent class orchestrates the full yield stacking pipeline: stake ETH via Lido, wrap to wstETH, deploy into the optimal venue based on current market conditions, monitor health, and rebalance automatically.
import asyncio
import logging
from dataclasses import dataclass, field
from typing import Dict, Optional
from web3 import AsyncWeb3
logger = logging.getLogger('LidoAgent')
@dataclass
class LidoConfig:
eth_to_stake: int # in wei
strategies: list = field(default_factory=lambda: ['curve', 'aave', 'eigenlayer'])
rebalance_interval: int = 3600 # seconds
depeg_exit_threshold: float = 0.01 # 1% depeg triggers exit
health_factor_min: float = 1.3 # Aave health factor threshold
eigenlayer_max_pct: float = 0.3 # max 30% restaked (illiquid)
class LidoAgent:
"""Complete Lido yield stacking agent."""
def __init__(self, w3: AsyncWeb3, account, config: LidoConfig):
self.w3 = w3
self.account = account
self.config = config
self.positions: Dict[str, int] = {}
self.depeg_monitor = StEthDepegMonitor(alert_threshold=config.depeg_exit_threshold)
async def initialize(self):
"""Stake ETH and distribute across strategies."""
# Step 1: Submit ETH to Lido
steth_amount = await self._stake_eth(self.config.eth_to_stake)
logger.info(f"Staked {self.config.eth_to_stake/1e18:.4f} ETH, received {steth_amount/1e18:.4f} stETH")
# Step 2: Wrap portion to wstETH
wrap_amount = int(steth_amount * 0.7) # keep 30% as stETH for Curve
wsteth_amount = await self._wrap_steth(wrap_amount)
# Step 3: Distribute across strategies
eigenlayer_amount = int(wsteth_amount * self.config.eigenlayer_max_pct)
aave_amount = wsteth_amount - eigenlayer_amount
curve_steth = steth_amount - wrap_amount
await asyncio.gather(
self._deploy_to_aave(aave_amount),
self._deploy_to_eigenlayer(eigenlayer_amount),
self._deploy_to_curve(curve_steth),
)
logger.info("Positions initialized across all strategies")
async def run(self):
"""Main agent loop: monitor, rebalance, and harvest."""
asyncio.create_task(self.depeg_monitor.monitor(self._handle_depeg_alert))
while True:
try:
await self._check_health()
await self._harvest_rewards()
await self._rebalance_if_needed()
except Exception as e:
logger.error(f"Agent error: {e}")
await asyncio.sleep(self.config.rebalance_interval)
async def _handle_depeg_alert(self, alert: dict):
"""React to stETH depeg events."""
if alert['severity'] == 'HIGH':
logger.warning(f"Severe depeg: stETH at {alert['price']:.4f} ETH. Exiting Aave position.")
await self._emergency_exit_aave()
else:
logger.info(f"Minor depeg: {alert['depeg_pct']:.2f}%. Monitoring.")
async def _check_health(self):
"""Check Aave health factor and top-up if needed."""
hf = await self._get_aave_health_factor()
if hf < self.config.health_factor_min:
repay_amount = await self._calc_repay_to_hf(1.5)
await self._repay_aave(repay_amount)
logger.warning(f"Health factor {hf:.3f} below min. Repaid {repay_amount/1e6:.2f} USDC")
async def _harvest_rewards(self):
"""Claim and compound CRV and other rewards."""
crv_claimable = await self._get_crv_claimable()
if crv_claimable > 100e18: # min 100 CRV to harvest
await self._claim_crv()
await self._compound_crv_to_steth(crv_claimable)
logger.info(f"Harvested and compounded {crv_claimable/1e18:.2f} CRV")
9. Full Yield Stack Analysis
Here is a complete view of the yield stack agents can build on top of stETH:
Maximum stETH Yield Stack (Annualized)
Gas costs will reduce this by 0.3-0.8% depending on position size and Ethereum gas prices. Positions under $50K equivalent may not be economical at current gas rates.
10. Risk Framework
Building a stETH yield stack exposes agents to multiple simultaneous risk vectors. A robust risk framework assigns weights to each:
| Risk | Likelihood | Impact | Mitigation |
|---|---|---|---|
| stETH depeg >5% | Low | High | Depeg monitor + auto-exit Aave |
| Lido validator slashing | Very Low | Medium | Lido insurance fund + diversify LSTs |
| Aave liquidation | Medium | High | HF monitoring + 1.4+ target HF |
| CRV price crash | Medium | Low | Auto-compound CRV to stETH immediately |
| EigenLayer slashing | Low | Medium | Cap restaking at 30% of position |
| Smart contract exploit | Very Low | Very High | Diversify protocols; use audited contracts only |
Agent Advantage: AI agents monitoring positions 24/7 at block-level granularity have a significant safety advantage over human DeFi participants. Automated health checks and instant reaction to depeg events dramatically reduce liquidation risk.
Run Your Agent on Purple Flea
Purple Flea provides financial primitives for AI agents: casino for entertainment, perpetuals for speculation, wallet for asset custody, domains for identity, faucet for test funds, and escrow for trustless payments.
Explore Purple Flea