The Example: Euler Vault Share Price
Throughout this guide, we’ll use a real example: protecting vault share prices in Euler’s Ethereum Vault Connector (EVC). This example demonstrates the full journey from protocol specification to working assertion. The invariant we’ll implement:“A vault’s share price should not decrease unless bad debt socialization occurs.”This protects depositors from economic attacks, protocol bugs, and malicious vault implementations while allowing legitimate protocol operations.
Step 1: Find and Define the Invariant
Where Do Invariants Come From?
Invariants typically come from:- Protocol specifications and whitepapers: The authoritative source for how the protocol should behave
- Security audits: Often document critical properties that must hold
- Your own analysis: Asking “what should never happen?”
Define the Invariant
Start by asking: What should never happen? For a vault, a decreasing share price means depositors are losing value. In pseudo-Solidity:Step 2: Identify Exceptions
Not every invariant violation is an attack. Protocol specifications often document legitimate scenarios where an invariant might not hold.Bad Debt Socialization
The Euler Vault Kit whitepaper describes a scenario called “bad debt socialization”:When an account in violation has had all of its collateral seized during a liquidation but still has a non-zero liability, this liability is called bad debt and is cancelled, socialising the loss to all current depositors in the vault.This is an intentional design decision to avoid bank-run scenarios. When bad debt is socialized, the share price decreases, but this is expected behavior, not an attack. The whitepaper also specifies how this is signaled:
To allow off-chain users to accurately track total borrows and internal balances using event logs, bad debt socialisation emits Repay and Withdraw logs, where the repay appears to come from the liquidator and the withdraw appears to come from address(0).
Refine the Invariant
Now we can refine our invariant to account for this exception:Step 3: Determine Where to Enforce
Before writing code, decide where to check the invariant. You have two options:- At each vault individually: Check share price on every vault contract
- At a central entry point: Check share price at the contract that routes calls to vaults
batch(), call(), and controlCollateral() functions.
Why check at the EVC?
- Single assertion covers all vaults
- Catches attacks regardless of which vault is targeted
- Simpler to maintain than per-vault assertions
Step 4: Choose Your Triggers
Now we need to specify when our assertion should run. Looking at the EVC interface, three functions can lead to vault interactions:batch(): Execute multiple operations atomicallycall(): Execute a single operation on a target contractcontrolCollateral(): Manage collateral positions
Step 5: Write the Assertion Logic
Now we implement the assertion. We’ll break this into three parts:Part A: Get the Vault Address
For each entry point, we need to extract which vault is being interacted with. Here’s how we do it forcall():
ph.getAssertionAdopter(): Get the contract address this assertion is protectingph.getCallInputs(): Get all calls to a specific function
Part B: Compare Pre and Post Share Price
The core logic compares share prices before and after the transaction:PhEvm.ForkId: Identifies the pre-transaction and post-transaction snapshotsph.loadStateAt(): Reads a storage slot at a specific snapshot
Part C: Detect Bad Debt Socialization
Finally, we check if a share price decrease is due to legitimate bad debt socialization:ph.getLogs(): Retrieve all logs emitted during the transaction
We check for both the
DebtSocialized event (added after the whitepaper was written) and the original Repay + Withdraw pattern documented in the spec. This defensive approach ensures we catch bad debt socialization even if there’s an edge case where the new event isn’t emitted.Step 6: Test Your Assertion
Before deploying, you should thoroughly test your assertion. Here are the three most common test cases for this invariant:- Normal operation: Share price increases or stays the same (should pass)
- Attack scenario: Share price decreases without bad debt (should fail)
- Legitimate exception: Share price decreases with bad debt socialization (should pass)
call() entry point. Tests use cl.assertion() to register the assertion before executing the transaction.
Test 1: Normal Operation (Should Pass)
Test 2: Attack Detection (Should Fail)
Test 3: Legitimate Exception (Should Pass)
Recap
You’ve learned the process of turning an invariant into an assertion:- Find the invariant: Check protocol specs, whitepapers, and audits
- Identify exceptions: Not every violation is an attack
- Determine where to enforce: Find the chokepoint where transactions flow
- Choose triggers: Specify which functions should invoke your assertion
- Write the logic: Use cheatcodes to compare pre/post state and handle exceptions
- Test thoroughly: Verify normal operations pass and attacks are caught
The complete example from this guide is available in our fork of the EVC repository.
Next Steps
Assertions Book
More real-world assertion examples
Cheatcodes Reference
Complete reference for assertion cheatcodes
Testing Assertions
How to test your assertions
Triggers
Learn about trigger types and optimization

