Install in Claude Code
Copygit clone --depth 1 https://github.com/PurpleAILAB/Vigilo /tmp/lsd-token-integration-patterns && cp -r /tmp/lsd-token-integration-patterns/packages/claude/skills/integration-patterns/lsd-tokens ~/.claude/skills/lsd-token-integration-patternsThen start a new Claude Code session; the skill loads automatically.
Definition
SKILL.md
# LSD Token Integration Patterns
This skill provides knowledge for auditing Liquid Staking Derivative token integrations.
## LSD Token Overview
| Token | Protocol | Type | Key Issue |
|-------|----------|------|-----------|
| stETH | Lido | Rebasing | Balance changes daily |
| wstETH | Lido | Non-rebasing | Wrapped stETH |
| rETH | Rocket Pool | Non-rebasing | Exchange rate varies |
| cbETH | Coinbase | Non-rebasing | Exchange rate varies |
| sfrxETH | Frax | Non-rebasing | Yield-bearing |
| frxETH | Frax | 1:1 pegged | No yield |
---
## stETH (Lido) Integration
### Rebasing Behavior
stETH balance increases daily when staking rewards are distributed.
```solidity
// Day 1: User deposits 100 stETH
// Day 2: Balance is now 100.01 stETH (after rebase)
// Day 3: Balance is now 100.02 stETH (after rebase)
```
### Vulnerable Pattern
```solidity
// DANGEROUS: Caches rebasing token balance
contract VulnerableVault {
mapping(address => uint256) public deposits;
function deposit(uint256 amount) external {
stETH.transferFrom(msg.sender, address(this), amount);
deposits[msg.sender] = amount; // WRONG: Amount will be stale!
}
function withdraw() external {
uint256 amount = deposits[msg.sender];
stETH.transfer(msg.sender, amount);
// User loses rebase rewards!
}
}
```
### Secure Pattern: Use Shares
```solidity
contract SecureVault {
mapping(address => uint256) public shares;
function deposit(uint256 stEthAmount) external {
// Transfer stETH
uint256 balanceBefore = stETH.balanceOf(address(this));
stETH.transferFrom(msg.sender, address(this), stEthAmount);
uint256 received = stETH.balanceOf(address(this)) - balanceBefore;
// Track shares, not amounts
uint256 sharesReceived = stETH.getSharesByPooledEth(received);
shares[msg.sender] += sharesReceived;
}
function withdraw() external {
uint256 userShares = shares[msg.sender];
shares[msg.sender] = 0;
// Convert shares back to stETH amount
uint256 stEthAmount = stETH.getPooledEthByShares(userShares);
stETH.transfer(msg.sender, stEthAmount);
}
}
```
### stETH-Specific Functions
```solidity
interface IStETH {
// Convert stETH amount to shares
function getSharesByPooledEth(uint256 _ethAmount)
external view returns (uint256);
// Convert shares to stETH amount
function getPooledEthByShares(uint256 _sharesAmount)
external view returns (uint256);
// Transfer using shares (avoids rounding)
function transferShares(address _recipient, uint256 _sharesAmount)
external returns (uint256);
// Get shares balance
function sharesOf(address _account) external view returns (uint256);
}
```
### stETH Transfer Issues
```solidity
// ISSUE: 1-2 wei rounding error on transfers
uint256 balanceBefore = stETH.balanceOf(address(this));
stETH.transferFrom(user, address(this), 1 ether);
uint256 received = stETH.balanceOf(address(this)) - balanceBefore;
// received might be 0.999999999999999998 ether (1-2 wei less)
// SOLUTION: Use transferShares for exact amounts
stETH.transferShares(recipient, sharesAmount);
```
---
## wstETH (Wrapped stETH)
### Why Use wstETH?
wstETH is non-rebasing, making it DeFi-compatible:
- Balance stays constant
- Value increases as stETH/wstETH rate changes
- Easier to integrate than stETH
### Conversion Functions
```solidity
interface IWstETH {
// Wrap stETH to wstETH
function wrap(uint256 _stETHAmount) external returns (uint256);
// Unwrap wstETH to stETH
function unwrap(uint256 _wstETHAmount) external returns (uint256);
// Get stETH amount for wstETH
function getStETHByWstETH(uint256 _wstETHAmount)
external view returns (uint256);
// Get wstETH amount for stETH
function getWstETHByStETH(uint256 _stETHAmount)
external view returns (uint256);
// Current exchange rate
function stEthPerToken() external view returns (uint256);
}
```
### Integration Pattern
```solidity
contract WstETHVault {
IERC20 public wstETH;
function deposit(uint256 amount) external {
// wstETH is non-rebasing, simple tracking works
wstETH.transferFrom(msg.sender, address(this), amount);
balances[msg.sender] += amount;
}
function getValueInETH(address user) public view returns (uint256) {
// Convert wstETH balance to ETH value
uint256 wstBalance = balances[user];
uint256 stEthAmount = IWstETH(address(wstETH))
.getStETHByWstETH(wstBalance);
// stETH is ~1:1 with ETH
return stEthAmount;
}
}
```
---
## rETH (Rocket Pool)
### Exchange Rate Behavior
rETH value increases relative to ETH as rewards accrue.
```solidity
interface IRocketTokenRETH {
// Get ETH value of rETH amount
function getEthValue(uint256 _rethAmount)
external view returns (uint256);
// Get rETH amount for ETH value
function getRethValue(uint256 _ethAmount)
external view returns (uint256);
// Current exchange rate (ETH per rETH)
function getExchangeRate() external view returns (uint256);
}
```
### Integration Considerations
```solidity
// rETH rate only increases, never decreases (no slashing... yet)
// But rate can be slightly stale
// Getting current value
uint256 ethValue = rETH.getEthValue(rethBalance);
// CAUTION: Exchange rate can be manipulated via Rocket Pool deposits
// Use oracle for price if security-critical
```
---
## cbETH (Coinbase)
### Exchange Rate
```solidity
interface ICbETH {
// Exchange rate: cbETH per ETH
function exchangeRate() external view returns (uint256);
}
// Get ETH value
uint256 ethValue = cbEthAmount * cbETH.exchangeRate() / 1e18;
```
### Known Issues
- Exchange rate updated by Coinbase (centralized)
- Rate can be briefly stale
- Consider oracle for critical valuations
---
## sfrxETH (Frax)
### Exchange Rate
```solidit