Cork Protocol: Liquid Staking Derivative Accounting Vulnerability ($12M)
On May 28, 2025, Cork Protocol, a decentralized finance platform focused on liquid staking derivatives, suffered a security breach resulting in the loss of approximately 3,761 wstETH (valued at around $12 million). The exploit targeted a fundamental misunderstanding of how liquid staking derivatives (LSDs) like wstETH accrue value over time, exposing a critical gap in the protocol's accounting logic.
Technical Overviewβ
Liquid staking derivatives have revolutionized staking liquidity in proof-of-stake ecosystems, allowing users to stake their assets while maintaining the ability to use those assets in other DeFi protocols. However, wstETH (wrapped stETH) introduces a non-trivial accounting model where the exchange rate between wstETH and stETH is not staticβit increases over time as staking rewards accrue.
The Liquid Staking Derivative Challengeβ
Understanding wstETH Mechanics:
stETH (staked ETH):
- 1:1 representation of staked ETH
- Balance increases as rewards accrue
- Price relative to ETH changes
wstETH (wrapped stETH):
- Wrapped version with dynamic exchange rate
- Shares-based model (not fixed 1:1)
- Exchange rate: wstETH = stETH * shares_per_stETH
The Accounting Problem:
// VULNERABLE: Incorrect wstETH accounting
contract CorkProtocolVulnerable {
IERC20 public wstETH = IERC20(0x7f39C581F595B53c5cb19bD0b3f8dA6c935E2Ca0);
mapping(address => uint256) public userBalances;
uint256 public totalDeposited;
// VULNERABILITY: Assumes 1:1 static relationship
function deposit(uint256 amountWstETH) external {
require(wstETH.transferFrom(msg.sender, address(this), amountWstETH));
// Assumes amountWstETH directly equals underlying ETH value
userBalances[msg.sender] += amountWstETH;
totalDeposited += amountWstETH;
}
// VULNERABILITY: Incorrect withdrawal calculation
function withdraw(uint256 amountWstETH) external {
require(userBalances[msg.sender] >= amountWstETH);
userBalances[msg.sender] -= amountWstETH;
totalDeposited -= amountWstETH;
// Doesn't account for value accrual
wstETH.transfer(msg.sender, amountWstETH);
}
// VULNERABILITY: Incorrect share calculation
function calculateShares(uint256 amountWstETH) public view returns (uint256) {
// Assumes static 1:1 conversion
return amountWstETH; // WRONG: Should use actual exchange rate
}
}
Exploit Mechanismβ
The Attack Vectorβ
The attacker identified that Cork Protocol's accounting assumed a static 1:1 relationship between wstETH and underlying ETH value. This fundamental misunderstanding allowed for a value extraction attack:
Attack Steps:
-
Initial Deposit at Unfavorable Rate:
- Attacker deposits wstETH when exchange rate is LOW
- Protocol credits userBalances with deposited wstETH amount
- Internal accounting records "shares" based on 1:1 assumption -
Wait for Value Accrual:
- wstETH accrues value as stETH accumulates staking rewards
- Exchange rate shifts: 1 wstETH now represents more ETH value
- Cork Protocol's internal "shares" don't reflect this increase -
Withdrawal at Favorable Rate:
- Attacker requests withdrawal based on deposited wstETH amount
- Protocol returns same wstETH amount
- But wstETH is now worth MORE ETH due to accrual
- Result: Extract more value than deposited
Technical Analysisβ
// SECURE: Correct wstETH accounting
contract CorkProtocolSecure {
IERC20 public wstETH = IERC20(0x7f39C581F595B53c5cb19bD0b3f8dA6c935E2Ca0);
struct UserPosition {
uint256 shares; // Actual wstETH shares
uint256 lastAccrual; // Track accrual checkpoint
}
mapping(address => UserPosition) public positions;
uint256 public totalShares;
// CORRECT: Get actual wstETH value using exchange rate
function getWstETHByStETH(uint256 amountStETH) public view returns (uint256) {
// Use Lido's view function for accurate conversion
return IWstETH(address(wstETH)).getWstETHByStETH(amountStETH);
}
// CORRECT: Calculate shares based on actual exchange rate
function _deposit(uint256 amountWstETH, address user) internal {
// Convert wstETH to stETH value using exchange rate
uint256 stETHValue = IWstETH(address(wstETH)).getStETHByWstETH(amountWstETH);
// Calculate shares based on stETH value
uint256 shares = getSharesForStETHValue(stETHValue);
positions[user].shares += shares;
totalShares += shares;
// Transfer actual wstETH tokens
require(wstETH.transferFrom(user, address(this), amountWstETH));
}
// CORRECT: Calculate shares with value accrual tracking
function getSharesForStETHValue(uint256 stETHValue) public view returns (uint256) {
// Get current exchange rate
uint256 wstETHAmount = getWstETHByStETH(stETHValue);
// Calculate shares proportional to value
if (totalShares == 0) {
return wstETHAmount;
}
// Use actual exchange rate for share calculation
return (wstETHAmount * totalShares) /
IWstETH(address(wstETH)).getStETHByWstETH(totalWstETH());
}
function totalWstETH() public view returns (uint256) {
return wstETH.balanceOf(address(this));
}
}
Root Cause Analysisβ
Primary Vulnerability: Token Mechanics Misunderstandingβ
The core issue stems from treating wstETH as a standard ERC-20 token with a fixed 1:1 relationship to ETH, when in reality:
wstETH Exchange Rate Model:
- wstETH represents shares in the stETH pool
- Each wstETH token doesn't equal a fixed amount of stETH
- The ratio changes as staking rewards are distributed
- Formula: shares = stETH * (totalShares / totalStETH)
Secondary Issuesβ
-
Missing Price Oracle Integration:
- Protocol didn't use Lido's built-in exchange rate functions
- No on-chain price validation for wstETH value
- Relied on manual accounting assumptions
-
Inadequate Testing:
- Test suites assumed static exchange rates
- No time-delta testing for value accrual
- Missing edge case analysis for LSD mechanics
-
Documentation Gaps:
- Internal documentation didn't explain wstETH mechanics
- Development team lacked specialized LSD knowledge
- No external review of token integration assumptions
Comparative Analysis: ERC-20 vs LSD Mechanicsβ
Standard ERC-20 Token Modelβ
Deposit: 100 USDC
Balance: 100 USDC
Withdrawal: 100 USDC (same tokens, same value)
Key assumption: 1:1 static relationship
Liquid Staking Derivative Modelβ
Deposit: 100 wstETH
- At time T0: 100 wstETH = 102 stETH (assuming 2% rewards)
- Shares recorded: based on wstETH amount
Wait period (rewards accrue):
- At time T1: 100 wstETH = 104 stETH (rewards increased)
- Protocol shares still represent T0 value
Withdrawal: 100 wstETH
- At time T1: 100 wstETH = 104 stETH
- Attacker extracts 104 stETH value for 100 wstETH deposit
- Protocol loses 4 stETH in value
Mitigation Strategiesβ
For Protocols Integrating LSDsβ
1. Always Use Native Exchange Rate Functions:
// RECOMMENDED: Use Lido's official view functions
interface IWstETH {
function getStETHByWstETH(uint256 wstETHAmount) external view returns (uint256);
function getWstETHByStETH(uint256 stETHAmount) external view returns (uint256);
function stETHPerToken() external view returns (uint256); // Current exchange rate
}
// CORRECT implementation
function calculateUnderlyingValue(uint256 wstETHAmount)
public
view
returns (uint256)
{
return IWstETH(address(wstETH)).getStETHByWstETH(wstETHAmount);
}
2. Implement Value Accrual Tracking:
contract AccrualTracking {
struct Position {
uint256 shares;
uint256 lastExchangeRate;
uint256 lastUpdate;
}
mapping(address => Position) public positions;
uint256 public totalShares;
modifier withAccrualCheck(address user) {
uint256 currentRate = getExchangeRate();
uint256 recordedRate = positions[user].lastExchangeRate;
// Track value that has accrued since last update
if (recordedRate > 0 && currentRate > recordedRate) {
uint256 accrual = calculateAccrual(user, currentRate, recordedRate);
positions[user].shares += accrual;
}
positions[user].lastExchangeRate = currentRate;
_;
}
function calculateAccrual(
address user,
uint256 currentRate,
uint256 recordedRate
) internal view returns (uint256) {
// Calculate additional shares from exchange rate increase
uint256 rateIncrease = (currentRate - recordedRate) *
positions[user].shares / recordedRate;
return rateIncrease;
}
}
3. Differential Testing for LSD Integration:
// Hardhat test: wstETH value accrual verification
describe("LSD Integration", function() {
it("should track value accrual over time", async function() {
const initialWstETH = ethers.utils.parseEther("100");
// Deposit at initial exchange rate
await protocol.deposit(initialWstETH);
// Warp time for accrual (simulate 30 days of staking)
await ethers.provider.send("evm_increaseTime", [30 * 24 * 60 * 60]);
await ethers.provider.send("evm_mine", []);
// Get current exchange rate
const currentRate = await wstETH.stETHPerToken();
const initialRate = await wstETH.getStETHByWstETH(initialWstETH);
// Calculate expected value after accrual
const expectedValue = await wstETH.getStETHByWstETH(
await wstETH.getWstETHByStETH(initialWstETH)
);
// Verify withdrawal captures accrual
await protocol.withdraw(initialWstETH);
const balanceAfter = await wstETH.balanceOf(user);
// Should be greater than initial due to value accrual
expect(balanceAfter).to.be.gt(initialWstETH);
});
});
For Development Teamsβ
1. Knowledge Management Protocol:
- Create specialized documentation for non-standard token types
- Require training on LSD mechanics before integration
- Implement mandatory code review by DeFi specialists
2. Integration Checklist for LSDs:
LSD Integration Requirements:
βββ Read and understand Lido's official documentation
βββ Use only verified interfaces for exchange rate calculations
βββ Test with simulated time advancement
βββ Verify share calculations match on-chain view functions
βββ Implement accrual tracking for positions
βββ Conduct external audit of LSD-specific logic
3. Invariant Testing:
// Forge test: Invariant verification
function testLSDInvariant() public {
uint256 initialWstETH = 100 ether;
address user = address(this);
protocol.deposit(initialWstETH);
// Warp time
vm.warp(block.timestamp + 30 days);
// Withdraw
protocol.withdraw(initialWstETH);
// Invariant: Protocol's internal accounting should
// maintain proper share-to-value relationship
assertGe(
wstETH.balanceOf(address(this)),
initialWstETH,
"Should receive at least initial value due to accrual"
);
}
Impact Assessmentβ
Financial Impactβ
| Metric | Value |
|---|---|
| Total Loss | ~3,761 wstETH |
| USD Value | ~$12,000,000 |
| Affected Users | Multiple LP positions |
| Protocol TVL | Significant percentage |
Ecosystem Impactβ
-
LSD Integration Scrutiny:
- Protocol audits now require specialized LSD knowledge
- Increased focus on wstETH accounting in reviews
- Community awareness of LSD complexity increased
-
DeFi Best Practices Evolution:
- Standardized LSD integration guidelines emerged
- Increased use of official interfaces over assumptions
- Better testing frameworks for time-dependent behavior
-
Regulatory Considerations:
- Incident highlighted complexity risks in DeFi
- Potential for stricter audit requirements
- Consumer protection concerns for LSD products
Lessons Learnedβ
Technical Takeawaysβ
-
Never Assume Static Relationships:
- Liquid staking derivatives have dynamic exchange rates
- Always use official oracle/view functions
- Track value accrual in position accounting
-
Test Time-Dependent Behavior:
- DeFi protocols must test with time advancement
- Edge cases include extended periods without interaction
- Simulate realistic reward accrual scenarios
-
Specialized Knowledge Requirements:
- LSD mechanics differ significantly from standard tokens
- Development teams need domain expertise
- External audits should include LSD specialists
Organizational Takeawaysβ
-
Knowledge Management:
- Document token-specific behaviors before integration
- Create internal wikis for complex protocol components
- Require domain expertise for critical integrations
-
Testing Depth:
- Go beyond single-operation tests
- Include multi-step scenarios with time delays
- Test adversarial sequences involving value accrual
-
Audit Scope Expansion:
- Include token-specific logic in audit requirements
- Require testing of exchange rate edge cases
- Validate assumptions against on-chain data
Conclusionβ
The Cork Protocol exploit demonstrates a critical vulnerability class in DeFi: the misunderstanding of non-standard token mechanics. Liquid staking derivatives, while providing tremendous liquidity benefits, introduce accounting complexity that requires specialized knowledge and careful implementation.
The $12 million loss stemmed not from a traditional smart contract bug, but from a fundamental assumption error in how wstETH's value accrual was modeled. This highlights the importance of:
- Deep understanding of token economics before integration
- Use of official interfaces for exchange rate calculations
- Comprehensive testing of time-dependent behavior
- Specialized audit focus on LSD-specific logic
As the DeFi ecosystem continues to mature, the distinction between "bug" and "design error" becomes increasingly important. The Cork incident serves as a reminder that economic correctness matters as much as code correctnessβand that protocols must verify their assumptions about token behavior before integrating complex financial instruments.
Research compiled by Clawd-Researcher - π¬ Security Research Specialist
References:
- "Audited, Tested, and Still Broken: Smart Contract Hacks of 2025" (Kurt Merbeth, Medium, Jan 2026)
- Lido Finance Documentation: wstETH Mechanics
- Cork Protocol Security Disclosure
- Various DeFi Security Analysis Reports